View on GitHub

RNN Probability Strategy

Overview

The RNN Probability strategy is a conversion of the MetaTrader expert RNN (barabashkakvn's edition). The original robot collects three RSI snapshots separated by the RSI period and feeds them into a hand-crafted probability lattice that emulates a recurrent neural network. The StockSharp port replicates this behaviour with the high-level candle subscription API, automatically converting the MetaTrader lots, price steps, and stop/target distances into StockSharp concepts.

Once the RSI value of the latest finished candle becomes available, the strategy looks back by one and two RSI periods to build a three-point history. These normalized readings are combined with the eight MetaTrader weights (Weight0Weight7) to produce a probability that the market should fall. The probability is remapped into the [-1; 1] range, and the sign determines whether to open a long or short position. Only one position at a time is maintained, matching the original Expert Advisor.

Trading logic

  1. Subscribe to the configured candle series and process the RelativeStrengthIndex indicator manually using the selected AppliedPrice input (open by default).
  2. Store the finished RSI values in a rolling buffer large enough to access the RSI reading from one and two full periods back.
  3. Normalise the three RSI values to the [0; 1] range and evaluate the neural network lattice:
    • The first branch (Weight0, Weight1, Weight2, Weight3) handles the case when the current RSI is in the lower half (below 50).
    • The second branch (Weight4, Weight5, Weight6, Weight7) handles the case when the current RSI is in the upper half.
  4. Transform the resulting probability into a trade signal between -1 and +1.
  5. If no position is open and the signal is negative, buy TradeVolume lots. If the signal is non-negative, sell TradeVolume lots instead.
  6. Optionally arm symmetric stop-loss and take-profit levels expressed in pips. The strategy automatically converts the pip distance to an absolute price offset, including the extra digit adjustment used by MetaTrader for 3- and 5-digit forex symbols.
  7. Log each decision with the RSI inputs, probability, and resulting signal, mirroring the chatty behaviour of the source expert.

Parameters

Name Type Default Description
CandleType DataType 1-hour time frame Primary candle series used for indicator updates and signal generation.
TradeVolume decimal 1 Lot size sent with each market order.
RsiPeriod int 9 Length of the RSI indicator. Also defines the distance between the historical RSI samples.
AppliedPrice AppliedPriceType Open Price component forwarded to the RSI (Open, Close, High, Low, Median, Typical, Weighted).
StopLossTakeProfitPips decimal 100 Pip distance for both stop-loss and take-profit. Set to zero to disable protective orders.
Weight0Weight7 decimal 6, 96, 90, 35, 64, 83, 66, 50 Probability weights applied to the eight lattice branches. Each value represents a percentage between 0 and 100.

Differences from the original MetaTrader expert

  • Email notifications were removed. StockSharp logs provide the same insight without relying on an SMTP server.
  • Position sizing is fixed to a single TradeVolume. Partial closures or incremental scaling are intentionally omitted to match the one-position design of the source code.
  • Indicator data is delivered through StockSharp's high-level candle subscription, eliminating manual CopyBuffer calls and pointer arithmetic.
  • Pip conversion uses the instrument's PriceStep and automatically compensates for 3/5-digit forex symbols instead of relying on hard-coded tick sizes.

Usage tips

  • Align TradeVolume with the instrument's minimum lot step before launching the strategy; the constructor also mirrors the value into Strategy.Volume.
  • Tune the eight weights during optimisation to adapt the neural network lattice to different markets. All weights are exposed as optimisation parameters.
  • Decrease StopLossTakeProfitPips or set it to zero when running on symbols with wide spreads or when using discretionary exits.
  • Add the strategy to a chart to visualise candles, RSI, and executed trades for easier validation of the neural-network output.

Indicators

  • One RelativeStrengthIndex calculated from the chosen applied price.
namespace StockSharp.Samples.Strategies;

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

using StockSharp.Algo;

/// <summary>
/// Probabilistic strategy converted from the RNN MetaTrader expert.
/// It feeds three delayed RSI readings into the original probability lattice and
/// trades in the direction suggested by the neural network output.
/// </summary>
public class RnnProbabilityStrategy : Strategy
{
	public enum AppliedPriceTypes
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}

	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<AppliedPriceTypes> _appliedPrice;
	private readonly StrategyParam<decimal> _stopLossTakeProfitPips;
	private readonly StrategyParam<decimal> _weight0;
	private readonly StrategyParam<decimal> _weight1;
	private readonly StrategyParam<decimal> _weight2;
	private readonly StrategyParam<decimal> _weight3;
	private readonly StrategyParam<decimal> _weight4;
	private readonly StrategyParam<decimal> _weight5;
	private readonly StrategyParam<decimal> _weight6;
	private readonly StrategyParam<decimal> _weight7;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private readonly List<decimal> _rsiHistory = new();
	private decimal _pipSize;

	/// <summary>
	/// Trade volume expressed in lots.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Averaging period for the RSI indicator.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Price source forwarded to the RSI indicator.
	/// </summary>
	public AppliedPriceTypes AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	/// <summary>
	/// Symmetric stop-loss and take-profit distance expressed in pips.
	/// </summary>
	public decimal StopLossTakeProfitPips
	{
		get => _stopLossTakeProfitPips.Value;
		set => _stopLossTakeProfitPips.Value = value;
	}

	/// <summary>
	/// Neural network weight for the (low, low, low) RSI combination.
	/// </summary>
	public decimal Weight0
	{
		get => _weight0.Value;
		set => _weight0.Value = value;
	}

	/// <summary>
	/// Neural network weight for the (low, low, high) RSI combination.
	/// </summary>
	public decimal Weight1
	{
		get => _weight1.Value;
		set => _weight1.Value = value;
	}

	/// <summary>
	/// Neural network weight for the (low, high, low) RSI combination.
	/// </summary>
	public decimal Weight2
	{
		get => _weight2.Value;
		set => _weight2.Value = value;
	}

	/// <summary>
	/// Neural network weight for the (low, high, high) RSI combination.
	/// </summary>
	public decimal Weight3
	{
		get => _weight3.Value;
		set => _weight3.Value = value;
	}

	/// <summary>
	/// Neural network weight for the (high, low, low) RSI combination.
	/// </summary>
	public decimal Weight4
	{
		get => _weight4.Value;
		set => _weight4.Value = value;
	}

	/// <summary>
	/// Neural network weight for the (high, low, high) RSI combination.
	/// </summary>
	public decimal Weight5
	{
		get => _weight5.Value;
		set => _weight5.Value = value;
	}

	/// <summary>
	/// Neural network weight for the (high, high, low) RSI combination.
	/// </summary>
	public decimal Weight6
	{
		get => _weight6.Value;
		set => _weight6.Value = value;
	}

	/// <summary>
	/// Neural network weight for the (high, high, high) RSI combination.
	/// </summary>
	public decimal Weight7
	{
		get => _weight7.Value;
		set => _weight7.Value = value;
	}

	/// <summary>
	/// Candle series used for indicator calculations and trading decisions.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="RnnProbabilityStrategy"/> class.
	/// </summary>
	public RnnProbabilityStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetDisplay("Trade Volume", "Lot size used for each market entry.", "General")
			.SetGreaterThanZero()
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 9)
			.SetDisplay("RSI Period", "Length of the RSI indicator feeding the neural network.", "Indicator")
			.SetRange(2, 200)
			;

		_appliedPrice = Param(nameof(AppliedPrice), AppliedPriceTypes.Open)
			.SetDisplay("Applied Price", "Price type forwarded to the RSI indicator.", "Indicator");

		_stopLossTakeProfitPips = Param(nameof(StopLossTakeProfitPips), 100m)
			.SetDisplay("Stop Loss & Take Profit (pips)", "Distance used for both stop-loss and take-profit levels.", "Risk")
			.SetRange(0m, 1000m)
			;

		_weight0 = Param(nameof(Weight0), 6m)
			.SetDisplay("Weight 0", "Probability weight applied when all RSI inputs are low.", "Model")
			.SetRange(0m, 100m)
			;

		_weight1 = Param(nameof(Weight1), 96m)
			.SetDisplay("Weight 1", "Probability weight for the (low, low, high) branch.", "Model")
			.SetRange(0m, 100m)
			;

		_weight2 = Param(nameof(Weight2), 90m)
			.SetDisplay("Weight 2", "Probability weight for the (low, high, low) branch.", "Model")
			.SetRange(0m, 100m)
			;

		_weight3 = Param(nameof(Weight3), 35m)
			.SetDisplay("Weight 3", "Probability weight for the (low, high, high) branch.", "Model")
			.SetRange(0m, 100m)
			;

		_weight4 = Param(nameof(Weight4), 64m)
			.SetDisplay("Weight 4", "Probability weight for the (high, low, low) branch.", "Model")
			.SetRange(0m, 100m)
			;

		_weight5 = Param(nameof(Weight5), 83m)
			.SetDisplay("Weight 5", "Probability weight for the (high, low, high) branch.", "Model")
			.SetRange(0m, 100m)
			;

		_weight6 = Param(nameof(Weight6), 66m)
			.SetDisplay("Weight 6", "Probability weight for the (high, high, low) branch.", "Model")
			.SetRange(0m, 100m)
			;

		_weight7 = Param(nameof(Weight7), 50m)
			.SetDisplay("Weight 7", "Probability weight for the (high, high, high) branch.", "Model")
			.SetRange(0m, 100m)
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for signal generation.", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

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

		_rsi = default;
		_rsiHistory.Clear();
		_pipSize = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = TradeVolume;

		_pipSize = CalculatePipSize();

		Unit stopLossUnit = null;
		Unit takeProfitUnit = null;

		if (StopLossTakeProfitPips > 0m && _pipSize > 0m)
		{
			var distance = StopLossTakeProfitPips * _pipSize;
			stopLossUnit = new Unit(distance, UnitTypes.Absolute);
			takeProfitUnit = new Unit(distance, UnitTypes.Absolute);
		}

		if (stopLossUnit != null || takeProfitUnit != null)
		{
			StartProtection(
				takeProfit: takeProfitUnit,
				stopLoss: stopLossUnit,
				isStopTrailing: false,
				useMarketOrders: true);
		}

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _rsi);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_rsi == null)
			return;

		if (RsiPeriod <= 0)
			return;

		var price = AppliedPrice switch
		{
			AppliedPriceTypes.Open => candle.OpenPrice,
			AppliedPriceTypes.High => candle.HighPrice,
			AppliedPriceTypes.Low => candle.LowPrice,
			AppliedPriceTypes.Close => candle.ClosePrice,
			AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceTypes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceTypes.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
		var rsiIndicatorValue = _rsi.Process(new DecimalIndicatorValue(_rsi, price, candle.OpenTime) { IsFinal = true });

		if (!_rsi.IsFormed || rsiIndicatorValue.IsEmpty)
			return;

		var rsiValue = rsiIndicatorValue.ToDecimal();

		_rsiHistory.Add(rsiValue);
		TrimHistory(_rsiHistory, GetHistoryLimit());

		var lastIndex = _rsiHistory.Count - 1;
		var delayedIndex = lastIndex - RsiPeriod;
		var delayedTwiceIndex = lastIndex - (2 * RsiPeriod);

		if (delayedIndex < 0 || delayedTwiceIndex < 0)
			return;

		var p1 = _rsiHistory[lastIndex] / 100m;
		var p2 = _rsiHistory[delayedIndex] / 100m;
		var p3 = _rsiHistory[delayedTwiceIndex] / 100m;

		var probability = CalculateProbability(p1, p2, p3);
		var signal = probability * 2m - 1m;

		LogInfo($"RSI inputs: p1={p1:F4}, p2={p2:F4}, p3={p3:F4}, probability={probability:F4}, signal={signal:F4}");

		if (TradeVolume <= 0m)
			return;

		if (signal < 0m)
		{
			// want long
			if (Position <= 0m)
			{
				var vol = Math.Abs(Position) + TradeVolume;
				BuyMarket(vol);
			}
		}
		else
		{
			// want short
			if (Position >= 0m)
			{
				var vol = Position + TradeVolume;
				SellMarket(vol);
			}
		}
	}

	private decimal CalculateProbability(decimal p1, decimal p2, decimal p3)
	{
		var pn1 = 1m - p1;
		var pn2 = 1m - p2;
		var pn3 = 1m - p3;

		var probability =
			pn1 * (pn2 * (pn3 * Weight0 + p3 * Weight1) +
			        p2 * (pn3 * Weight2 + p3 * Weight3)) +
			p1 * (pn2 * (pn3 * Weight4 + p3 * Weight5) +
			        p2 * (pn3 * Weight6 + p3 * Weight7));

		return probability / 100m;
	}

	private int GetHistoryLimit()
	{
		return Math.Max((2 * RsiPeriod) + 5, RsiPeriod + 1);
	}

	private static void TrimHistory<T>(List<T> source, int maxSize)
	{
		if (maxSize <= 0)
			return;

		if (source.Count <= maxSize)
			return;

		var removeCount = source.Count - maxSize;
		source.RemoveRange(0, removeCount);
	}

	private decimal CalculatePipSize()
	{
		if (Security == null)
			return 0m;

		var step = Security.PriceStep ?? 0m;
		if (step <= 0m)
			return 0m;

		var decimals = GetDecimalPlaces(step);
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		value = Math.Abs(value);

		var decimals = 0;

		while (value != Math.Truncate(value) && decimals < 10)
		{
			value *= 10m;
			decimals++;
		}

		return decimals;
	}
}