This strategy replicates the MetaTrader 5 "Universal Signal" expert using StockSharp high-level APIs. It evaluates eight weighted market patterns and aggregates them into a single composite score. When the score crosses configurable thresholds the strategy opens or closes long and short positions, optionally using pending limit orders that expire after a set number of bars.
Strategy Parameters
CandleType – candle data used for the analysis.
SignalThresholdOpen – minimum composite score required to open a position.
SignalThresholdClose – opposing score required to exit an existing position.
PriceLevel – price offset for placing pending limit entries (0 means market execution).
StopLevel / TakeLevel – absolute stop-loss and take-profit distances used by the built-in protection module.
SignalExpiration – number of bars after which still-active pending entries are cancelled.
Pattern0Weight … Pattern7Weight – weight applied to each pattern before aggregation.
UniversalWeight – final multiplier applied to the sum of all pattern contributions.
ShortMaPeriod, LongMaPeriod, RsiPeriod, BollingerPeriod, BollingerWidth, TrendSmaPeriod, VolumeSmaPeriod – indicator settings used inside the pattern checks.
Trading Logic
Subscribe to the configured candle stream and bind EMA, RSI, MACD Signal, Bollinger Bands, and supporting SMAs.
After every finished candle, compute eight boolean patterns (trend alignment, RSI momentum, MACD histogram, Bollinger positioning, candle direction, and volume expansion).
Multiply each pattern by its weight, sum the contributions, and apply the global weight to obtain the final score.
Close open positions when the score crosses the closing threshold in the opposite direction.
Open new long or short positions when the score exceeds the opening threshold. If PriceLevel is positive, submit a limit order offset by the configured distance and cancel it automatically after SignalExpiration bars.
StartProtection sets fixed stop-loss and take-profit levels for all positions using StockSharp's risk management helpers.
The conversion keeps the flexible weighting workflow of the original MQL5 expert while following StockSharp coding conventions and indicator-based processing.
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Universal Signal Demo strategy: Multi-indicator scoring.
/// Combines RSI and EMA signals to generate aggregate score for entries.
/// </summary>
public class UniversalSignalDemoStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _signalCooldownCandles;
private int _prevScore;
private int _candlesSinceTrade;
private bool _hasPrevScore;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int RsiPeriod { get => _rsiPeriod.Value; set => _rsiPeriod.Value = value; }
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }
public UniversalSignalDemoStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_rsiPeriod = Param(nameof(RsiPeriod), 21)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "RSI period", "Indicators");
_emaPeriod = Param(nameof(EmaPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA period", "Indicators");
_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevScore = 0;
_candlesSinceTrade = SignalCooldownCandles;
_hasPrevScore = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevScore = 0;
_candlesSinceTrade = SignalCooldownCandles;
_hasPrevScore = false;
var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(rsi, ema, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal rsiValue, decimal emaValue)
{
if (candle.State != CandleStates.Finished) return;
if (_candlesSinceTrade < SignalCooldownCandles)
_candlesSinceTrade++;
var close = candle.ClosePrice;
var score = 0;
// RSI signal
if (rsiValue < 30) score += 2;
else if (rsiValue < 45) score += 1;
else if (rsiValue > 70) score -= 2;
else if (rsiValue > 55) score -= 1;
// EMA signal
if (close > emaValue) score += 1;
else if (close < emaValue) score -= 1;
// Candle direction
if (candle.ClosePrice > candle.OpenPrice) score += 1;
else if (candle.ClosePrice < candle.OpenPrice) score -= 1;
var crossedUp = (!_hasPrevScore || _prevScore < 2) && score >= 2;
var crossedDown = (!_hasPrevScore || _prevScore > -2) && score <= -2;
if (crossedUp && Position <= 0 && _candlesSinceTrade >= SignalCooldownCandles)
{
BuyMarket();
_candlesSinceTrade = 0;
}
else if (crossedDown && Position >= 0 && _candlesSinceTrade >= SignalCooldownCandles)
{
SellMarket();
_candlesSinceTrade = 0;
}
_prevScore = score;
_hasPrevScore = true;
}
}
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.Indicators import RelativeStrengthIndex, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class universal_signal_demo_strategy(Strategy):
def __init__(self):
super(universal_signal_demo_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._rsi_period = self.Param("RsiPeriod", 21)
self._ema_period = self.Param("EmaPeriod", 50)
self._signal_cooldown_candles = self.Param("SignalCooldownCandles", 4)
self._prev_score = 0
self._candles_since_trade = 4
self._has_prev_score = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@RsiPeriod.setter
def RsiPeriod(self, value):
self._rsi_period.Value = value
@property
def EmaPeriod(self):
return self._ema_period.Value
@EmaPeriod.setter
def EmaPeriod(self, value):
self._ema_period.Value = value
@property
def SignalCooldownCandles(self):
return self._signal_cooldown_candles.Value
@SignalCooldownCandles.setter
def SignalCooldownCandles(self, value):
self._signal_cooldown_candles.Value = value
def OnReseted(self):
super(universal_signal_demo_strategy, self).OnReseted()
self._prev_score = 0
self._candles_since_trade = self.SignalCooldownCandles
self._has_prev_score = False
def OnStarted2(self, time):
super(universal_signal_demo_strategy, self).OnStarted2(time)
self._prev_score = 0
self._candles_since_trade = self.SignalCooldownCandles
self._has_prev_score = False
rsi = RelativeStrengthIndex()
rsi.Length = self.RsiPeriod
ema = ExponentialMovingAverage()
ema.Length = self.EmaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(rsi, ema, self._process_candle).Start()
def _process_candle(self, candle, rsi_value, ema_value):
if candle.State != CandleStates.Finished:
return
if self._candles_since_trade < self.SignalCooldownCandles:
self._candles_since_trade += 1
close = float(candle.ClosePrice)
rsi_val = float(rsi_value)
ema_val = float(ema_value)
score = 0
# RSI signal
if rsi_val < 30:
score += 2
elif rsi_val < 45:
score += 1
elif rsi_val > 70:
score -= 2
elif rsi_val > 55:
score -= 1
# EMA signal
if close > ema_val:
score += 1
elif close < ema_val:
score -= 1
# Candle direction
if float(candle.ClosePrice) > float(candle.OpenPrice):
score += 1
elif float(candle.ClosePrice) < float(candle.OpenPrice):
score -= 1
crossed_up = (not self._has_prev_score or self._prev_score < 2) and score >= 2
crossed_down = (not self._has_prev_score or self._prev_score > -2) and score <= -2
if crossed_up and self.Position <= 0 and self._candles_since_trade >= self.SignalCooldownCandles:
self.BuyMarket()
self._candles_since_trade = 0
elif crossed_down and self.Position >= 0 and self._candles_since_trade >= self.SignalCooldownCandles:
self.SellMarket()
self._candles_since_trade = 0
self._prev_score = score
self._has_prev_score = True
def CreateClone(self):
return universal_signal_demo_strategy()