Открыть на GitHub

Стратегия RNN Probability

Обзор

Стратегия RNN Probability — это конвертация эксперта MetaTrader RNN (barabashkakvn's edition). Оригинальный робот считывает три значения RSI, разделённых длиной индикатора, и прогоняет их через решётку вероятностей, имитирующую рекуррентную нейросеть. Порт на StockSharp воспроизводит ту же логику через подписку на свечи высокого уровня, автоматически переводя понятия MetaTrader (лоты, пункты, стопы) в объекты StockSharp.

После появления значения RSI для последней завершённой свечи стратегия обращается к значениям RSI на одну и две длины индикатора назад. Нормированные показания объединяются с восемью весами (Weight0Weight7), заданными в исходном советнике, чтобы получить вероятность снижения цены. Вероятность преобразуется в диапазон [-1; 1], и знак результата определяет открытие длинной или короткой позиции. Одновременно держится только одна позиция, что соответствует оригинальной реализации.

Логика торговли

  1. Подписаться на выбранный тип свечей и вручную обновлять индикатор RelativeStrengthIndex, используя параметр AppliedPrice (по умолчанию — цена открытия).
  2. Сохранять завершённые значения RSI в скользящем буфере, достаточном для доступа к показаниям на одну и две длины индикатора назад.
  3. Нормировать три значения RSI в диапазон [0; 1] и вычислить решётку вероятностей:
    • Первая ветка (Weight0, Weight1, Weight2, Weight3) активна, если текущее значение RSI ниже середины диапазона (ниже 50).
    • Вторая ветка (Weight4, Weight5, Weight6, Weight7) активна, если текущее значение RSI выше середины.
  4. Преобразовать полученную вероятность в торговый сигнал между -1 и +1.
  5. Если позиция отсутствует и сигнал отрицательный, купить TradeVolume лотов. Если сигнал неотрицательный, продать TradeVolume лотов.
  6. При необходимости активировать симметричные стоп-лосс и тейк-профит в пунктах. Стратегия автоматически пересчитывает расстояние в абсолютное смещение цены и учитывает поправку MetaTrader для 3- и 5-значных форекс-котировок.
  7. Записывать в журнал RSI-входы, вероятность и итоговый сигнал, сохраняя информативность исходного советника.

Параметры

Имя Тип Значение по умолчанию Описание
CandleType DataType Таймфрейм 1 час Основная серия свечей для расчётов и сигналов.
TradeVolume decimal 1 Объём заявки в лотах.
RsiPeriod int 9 Период RSI. Определяет интервал между историческими выборками.
AppliedPrice AppliedPriceType Open Компонент цены, подаваемый в RSI (Open, Close, High, Low, Median, Typical, Weighted).
StopLossTakeProfitPips decimal 100 Расстояние до стоп-лосса и тейк-профита в пунктах. Ноль отключает защитные ордера.
Weight0Weight7 decimal 6, 96, 90, 35, 64, 83, 66, 50 Весовые коэффициенты решётки вероятностей. Каждое значение — процент от 0 до 100.

Отличия от оригинального эксперта MetaTrader

  • Удалены email-уведомления — детальный лог StockSharp обеспечивает ту же информативность без SMTP.
  • Объём позиции фиксирован TradeVolume. Частичное закрытие и наращивание позиции не реализованы, как и в исходнике.
  • Данные индикатора поступают через высокоуровневую подписку на свечи, что исключает прямые вызовы CopyBuffer и ручную работу с массивами.
  • Конвертация пунктов использует PriceStep инструмента и автоматически учитывает дополнительные знаки, вместо жёстко заданных размеров тика.

Рекомендации по использованию

  • Перед запуском согласуйте TradeVolume с минимальным шагом лота инструмента; значение также копируется в Strategy.Volume.
  • Настраивайте восемь весов в процессе оптимизации, чтобы адаптировать решётку вероятностей под другие рынки.
  • Уменьшайте StopLossTakeProfitPips или ставьте ноль при работе с широкими спредами или ручными выходами.
  • Добавьте стратегию на график, чтобы наблюдать свечи, RSI и сделки и быстрее проверять корректность сигналов.

Индикаторы

  • Один RelativeStrengthIndex, рассчитанный по выбранной цене.
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;
	}
}