The strategy is a C# port of the MetaTrader expert advisor Testinator v1.30a. It opens only long positions and manages them as a basket. Each new buy is permitted only when a configurable set of technical filters returns "true" and the price has advanced by a minimum number of pips. The exit logic mirrors the entry logic by using another filter mask. The original EA also relied on daily ATR measurements for risk management, therefore the port subscribes to daily candles in addition to the primary timeframe.
Trading logic
Entry filter mask (parameter BuySequence)
The mask uses the lower nine bits. A bit that is set must satisfy the corresponding test on the previous finished candle.
Bit
Condition
1
EMA(12) is above SMA(14).
2
EMA(50) stays below the lows of the last three candles.
4
Previous low is below the lower Bollinger band (20, 2).
8
ADX(14) is above the -DI and +DI is stronger than -DI.
16
Stochastic (16, 4, 8) has %K above %D and %D above 80.
32
Williams %R(14) is greater than -20.
64
MACD(12, 26, 9) line is above the signal line.
128
Ichimoku shows Senkou Span A above Span B, Tenkan above Kijun, and the previous low above Span A.
256
RSI (period RsiEntryPeriod) is above RsiEntryLevel and rising relative to the previous value.
Exit filter mask (parameter CloseBuySequence)
Bit
Condition
1
SMA(14) is above EMA(12).
2
EMA(50) is above the highs of the last three candles.
4
Previous high is above the upper exit Bollinger band (BollingerCloseLength, BollingerCloseDeviation).
8
-DI is above +DI.
16
Stochastic %D is below 80.
32
Williams %R(14) is less than -80.
64
MACD line is below the signal line.
128
Ichimoku Senkou Span B is above Senkou Span A.
256
RSI (period RsiClosePeriod) is below RsiCloseLevel.
A basket is extended only if all active entry bits return true, the number of buys is below MaxBuys, and the last fill price is at least StepPips away. The basket is flattened whenever the exit mask passes or when protective levels are triggered.
Session control and risk management
Trading takes place only between TradeStartHour and TradeStartHour + TradeDurationHours - 1 (Eastern European Time). If the window is closed and the basket is in profit, all buys are closed.
The protective stop and take-profit distances are expressed in pips. Setting a value to -1 disables it, while 0 activates the ATR multiplier (StopRatio, TakeRatio).
The trailing stop uses the same ATR logic through StartTrailPips, TrailStepPips, StartTrailRatio, and TrailStepRatio.
The strategy computes daily ATR(15) values on D1 candles to keep the behaviour identical to the EA.
Parameters
TradeVolume – lot size (volume) for every market buy.
BuySequence / CloseBuySequence – bit masks that enable individual indicator filters.
MaxBuys – maximum number of open buys handled as a basket.
StepPips – minimum price progress (pips) before adding to the basket.
TradeStartHour, TradeDurationHours – defines the daily trading window.
TakeProfitPips, StopLossPips – fixed protective levels (negative disables, zero switches to ATR ratios).
StartTrailPips, TrailStepPips – trailing start distance and step (negative disables, zero uses ATR ratios).
TakeRatio, StopRatio, StartTrailRatio, TrailStepRatio – ATR multipliers used when the fixed value equals zero.
RsiEntryLevel, RsiEntryPeriod – RSI threshold and period for the entry mask.
RsiCloseLevel, RsiClosePeriod – RSI threshold and period for the exit mask.
BollingerCloseLength, BollingerCloseDeviation – parameters of the exit Bollinger bands.
CandleType – timeframe of the working candles (daily candles are subscribed automatically for ATR).
Notes
The port keeps the basket accounting model from the original EA: all orders are buys and only market orders are used.
The logic intentionally stores previous indicator values to mimic the "bar[1]" checks from MetaTrader.
The strategy ignores the unused inputs of the EA (TakeAsBasket, StopAsBasket, etc.) because they did not affect the MQL logic.
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Testinator strategy: RSI-based entry with EMA trend filter.
/// Buys when RSI rises above threshold and price is above EMA.
/// Sells when RSI drops below threshold and price is below EMA.
/// </summary>
public class TestinatorStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<decimal> _rsiBuyLevel;
private readonly StrategyParam<decimal> _rsiSellLevel;
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 decimal RsiBuyLevel { get => _rsiBuyLevel.Value; set => _rsiBuyLevel.Value = value; }
public decimal RsiSellLevel { get => _rsiSellLevel.Value; set => _rsiSellLevel.Value = value; }
public TestinatorStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "RSI period", "Indicators");
_emaPeriod = Param(nameof(EmaPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA trend filter period", "Indicators");
_rsiBuyLevel = Param(nameof(RsiBuyLevel), 55m)
.SetDisplay("RSI Buy Level", "RSI threshold for buy signal", "Signals");
_rsiSellLevel = Param(nameof(RsiSellLevel), 45m)
.SetDisplay("RSI Sell Level", "RSI threshold for sell signal", "Signals");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
decimal? prevClose = null;
decimal? prevEma = null;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(rsi, ema, (candle, rsiVal, emaVal) =>
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
if (prevClose.HasValue && prevEma.HasValue)
{
var crossUp = prevClose.Value <= prevEma.Value && close > emaVal;
var crossDown = prevClose.Value >= prevEma.Value && close < emaVal;
if (crossUp && rsiVal > RsiBuyLevel && Position <= 0)
BuyMarket();
else if (crossDown && rsiVal < RsiSellLevel && Position >= 0)
SellMarket();
}
prevClose = close;
prevEma = emaVal;
})
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
}
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 testinator_strategy(Strategy):
def __init__(self):
super(testinator_strategy, self).__init__()
self._rsi_period = self.Param("RsiPeriod", 14) \
.SetDisplay("RSI Period", "RSI period", "Indicators")
self._ema_period = self.Param("EmaPeriod", 50) \
.SetDisplay("EMA Period", "EMA trend filter period", "Indicators")
self._rsi_buy_level = self.Param("RsiBuyLevel", 55.0) \
.SetDisplay("RSI Buy Level", "RSI threshold for buy signal", "Signals")
self._rsi_sell_level = self.Param("RsiSellLevel", 45.0) \
.SetDisplay("RSI Sell Level", "RSI threshold for sell signal", "Signals")
self._rsi = None
self._ema = None
self._prev_close = None
self._prev_ema = None
@property
def rsi_period(self):
return self._rsi_period.Value
@property
def ema_period(self):
return self._ema_period.Value
@property
def rsi_buy_level(self):
return self._rsi_buy_level.Value
@property
def rsi_sell_level(self):
return self._rsi_sell_level.Value
def OnReseted(self):
super(testinator_strategy, self).OnReseted()
self._rsi = None
self._ema = None
self._prev_close = None
self._prev_ema = None
def OnStarted2(self, time):
super(testinator_strategy, self).OnStarted2(time)
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.rsi_period
self._ema = ExponentialMovingAverage()
self._ema.Length = self.ema_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(30)))
subscription.Bind(self._rsi, self._ema, self._process_candle)
subscription.Start()
def _process_candle(self, candle, rsi_value, ema_value):
if candle.State != CandleStates.Finished:
return
if not self._rsi.IsFormed or not self._ema.IsFormed:
return
close = float(candle.ClosePrice)
rsi_val = float(rsi_value)
ema_val = float(ema_value)
if self._prev_close is not None and self._prev_ema is not None:
cross_up = self._prev_close <= self._prev_ema and close > ema_val
cross_down = self._prev_close >= self._prev_ema and close < ema_val
if cross_up and rsi_val > self.rsi_buy_level and self.Position <= 0:
self.BuyMarket()
elif cross_down and rsi_val < self.rsi_sell_level and self.Position >= 0:
self.SellMarket()
self._prev_close = close
self._prev_ema = ema_val
def CreateClone(self):
return testinator_strategy()