Открыть на GitHub

Стратегия Poker Show

Обзор

Стратегия Poker Show представляет собой прямую конверсию советника MetaTrader 5 «Poker_SHOW». Она сочетает фильтр тренда на основе скользящей средней с вероятностным триггером, имитирующим вытягивание покерной комбинации. Сделка исполняется только тогда, когда случайно сгенерированное число оказывается меньше выбранного порога покерной комбинации. Такой подход обеспечивает редкие входы и удерживает стратегию в направлении текущего тренда, определяемого скользящей средней.

Алгоритм рассчитан на работу с одним инструментом и использует стандартные свечи по времени. Решения пересматриваются после формирования каждой свечи, что соответствует исходному советнику, реагирующему на открытие нового бара.

Основная логика

  1. Трендовый фильтр по скользящей средней

    • Используется настраиваемая скользящая средняя (SMA, EMA, SMMA или LWMA), вычисляемая по выбранному типу цены (close, open, high, low, median, typical, weighted).
    • Индикатор может быть сдвинут вперёд по оси времени, чтобы повторить параметр ma_shift из MetaTrader. Для принятия решений берётся значение предыдущей полностью сформированной свечи.
  2. Вероятностный фильтр

    • Для каждого направления на каждом баре генерируется независимое случайное число от 0 до 32 767.
    • Это число сравнивается с выбранной покерной комбинацией. Чем выше комбинация (например, стрит-флеш), тем меньше порог и тем реже будут сделки. Низшие комбинации (например, пара) приводят к более частым входам.
  3. Правила входа

    • Лонг открывается, если скользящая средняя находится выше цены минимум на заданное расстояние. При включённом параметре Reverse Signals условие инвертируется.
    • Шорт открывается, если средняя находится ниже цены на то же расстояние. При инверсии сигналов логика зеркально меняется.
    • В любой момент времени поддерживается только одна позиция. Открытие позиции в противоположном направлении автоматически закрывает текущую и разворачивает экспозицию.
  4. Управление рисками

    • Стоп-лосс и тейк-профит задаются в шагах цены относительно цены входа. Нулевое значение отключает соответствующий уровень.
    • Срабатывание стопа или цели проверяется на закрытии каждой свечи. При достижении уровня позиция закрывается, а внутренние отметки риска сбрасываются.
  5. Защита позиции

    • При запуске активируется встроенный модуль защиты StockSharp, что помогает ограничить неожиданные убытки при ручном использовании.

Параметры

Параметр Описание
Poker Combination Порог вероятности, который должен превысить случайное число, чтобы открыть сделку. Соответствует классическим покерным комбинациям от стрит-флеша (самая редкая) до пары (самая частая).
Volume Объём ордера в лотах. Используется как для новых входов, так и для разворота позиции.
Stop Loss Расстояние от цены входа до защитного стопа в шагах цены. Ноль — стоп отключён.
Take Profit Расстояние до цели в шагах цены. Ноль — тейк отключён.
Enable Buy Разрешает открытие длинных позиций.
Enable Sell Разрешает открытие коротких позиций.
MA Distance Минимально необходимое расстояние между значением скользящей средней и ценой. Выступает в роли фильтра тренда.
MA Period Количество баров в расчёте скользящей средней.
MA Shift Горизонтальный сдвиг скользящей средней (в барах), полностью повторяет параметр ma_shift исходного советника.
MA Method Тип сглаживания скользящей средней: простая, экспоненциальная, сглаженная или линейно взвешенная.
Applied Price Тип цены свечи, используемый в расчёте средней.
Reverse Signals Инвертирует сравнение средней и цены, меняя местами условия для лонгов и шортов.
Candle Type Таймфрейм используемых свечей. По умолчанию — один час, как в оригинале.

Примечания

  • Из-за вероятностного фильтра стратегия сильно зависит от случайности. Для оценки устойчивости рекомендуется проводить несколько прогоночных тестов или анализ Монте-Карло.
  • Управление рисками происходит только на закрытии свечи. При сильных внутридневных движениях цена может выходить за уровни стопа или тейка до того, как стратегия успеет отреагировать. Если это критично, используйте более мелкие таймфреймы.
  • Для максимально точного совпадения с MetaTrader следует убедиться, что у инструмента совпадают размер контракта и шаг цены.
  • Заявки отправляются рыночными ордерами (BuyMarket и SellMarket), как и в исходном советнике. Обработка проскальзывания выполняется инфраструктурой StockSharp.
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;

namespace StockSharp.Samples.Strategies;



/// <summary>
/// Strategy that emulates the Poker_SHOW MetaTrader 5 expert advisor.
/// Combines a moving average trend filter with random trade triggering and fixed risk targets.
/// </summary>
public class PokerShowStrategy : Strategy
{
	/// <summary>
	/// Poker combination thresholds used to gate random trade execution.
	/// </summary>
	public enum PokerCombinations
	{
		/// <summary>
		/// Straight flush probability threshold.
		/// </summary>
		Royal0 = 127,

		/// <summary>
		/// Four of a kind probability threshold.
		/// </summary>
		Royal1 = 255,

		/// <summary>
		/// Full house probability threshold.
		/// </summary>
		Royal2 = 511,

		/// <summary>
		/// Flush probability threshold.
		/// </summary>
		Royal3 = 1023,

		/// <summary>
		/// Straight probability threshold.
		/// </summary>
		Royal4 = 2047,

		/// <summary>
		/// Three of a kind probability threshold.
		/// </summary>
		Royal5 = 4095,

		/// <summary>
		/// Two pairs probability threshold.
		/// </summary>
		Royal6 = 8191,

		/// <summary>
		/// One pair probability threshold.
		/// </summary>
		Couple = 16383
	}

	/// <summary>
	/// Moving average smoothing methods supported by the strategy.
	/// </summary>
	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Sma = 0,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Ema = 1,

		/// <summary>
		/// Smoothed moving average.
		/// </summary>
		Smma = 2,

		/// <summary>
		/// Linear weighted moving average.
		/// </summary>
		Lwma = 3
	}

	/// <summary>
	/// Price sources emulating MetaTrader applied price options.
	/// </summary>
	public enum AppliedPriceses
	{
		/// <summary>
		/// Use close price.
		/// </summary>
		Close = 0,

		/// <summary>
		/// Use open price.
		/// </summary>
		Open = 1,

		/// <summary>
		/// Use high price.
		/// </summary>
		High = 2,

		/// <summary>
		/// Use low price.
		/// </summary>
		Low = 3,

		/// <summary>
		/// Use median price (high + low) / 2.
		/// </summary>
		Median = 4,

		/// <summary>
		/// Use typical price (high + low + close) / 3.
		/// </summary>
		Typical = 5,

		/// <summary>
		/// Use weighted price (high + low + 2 * close) / 4.
		/// </summary>
		Weighted = 6
	}

	private readonly StrategyParam<PokerCombinations> _combination;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<bool> _enableBuy;
	private readonly StrategyParam<bool> _enableSell;
	private readonly StrategyParam<int> _distancePoints;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MovingAverageMethods> _maMethod;
	private readonly StrategyParam<AppliedPriceses> _appliedPrice;
	private readonly StrategyParam<bool> _reverseSignal;
	private readonly StrategyParam<DataType> _candleType;

	private IIndicator _ma;
	private readonly List<decimal> _maHistory = [];

	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _priceStep;

	/// <summary>
	/// Minimum poker hand value that must be greater than a random draw to enable a trade.
	/// </summary>
	public PokerCombinations Combination
	{
		get => _combination.Value;
		set => _combination.Value = value;
	}

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

	/// <summary>
	/// Stop loss distance measured in price steps (points).
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance measured in price steps (points).
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Allow long entries.
	/// </summary>
	public bool EnableBuy
	{
		get => _enableBuy.Value;
		set => _enableBuy.Value = value;
	}

	/// <summary>
	/// Allow short entries.
	/// </summary>
	public bool EnableSell
	{
		get => _enableSell.Value;
		set => _enableSell.Value = value;
	}

	/// <summary>
	/// Minimum required distance between price and moving average in points.
	/// </summary>
	public int DistancePoints
	{
		get => _distancePoints.Value;
		set => _distancePoints.Value = value;
	}

	/// <summary>
	/// Moving average period.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Horizontal moving average shift in bars.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average smoothing method.
	/// </summary>
	public MovingAverageMethods MaMethod
	{
		get => _maMethod.Value;
		set => _maMethod.Value = value;
	}

	/// <summary>
	/// Price source for moving average calculations.
	/// </summary>
	public AppliedPriceses AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	/// <summary>
	/// Reverse the signal direction.
	/// </summary>
	public bool ReverseSignal
	{
		get => _reverseSignal.Value;
		set => _reverseSignal.Value = value;
	}

	/// <summary>
	/// Candle type used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="PokerShowStrategy"/>.
	/// </summary>
	public PokerShowStrategy()
	{
		_combination = Param(nameof(Combination), PokerCombinations.Couple)
		.SetDisplay("Poker Combination", "Probability gate for opening trades", "Signals");

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Order volume in lots", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 50)
		.SetDisplay("Stop Loss", "Stop loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 150)
		.SetDisplay("Take Profit", "Take profit distance in price steps", "Risk");

		_enableBuy = Param(nameof(EnableBuy), true)
		.SetDisplay("Enable Buy", "Allow opening long positions", "Signals");

		_enableSell = Param(nameof(EnableSell), true)
		.SetDisplay("Enable Sell", "Allow opening short positions", "Signals");

		_distancePoints = Param(nameof(DistancePoints), 50)
		.SetDisplay("MA Distance", "Minimum distance between price and MA", "Signals");

		_maPeriod = Param(nameof(MaPeriod), 24)
		.SetGreaterThanZero()
		.SetDisplay("MA Period", "Length of the moving average", "Moving Average");

		_maShift = Param(nameof(MaShift), 0)
		.SetDisplay("MA Shift", "Horizontal shift applied to the moving average", "Moving Average");

		_maMethod = Param(nameof(MaMethod), MovingAverageMethods.Ema)
		.SetDisplay("MA Method", "Moving average smoothing type", "Moving Average");

		_appliedPrice = Param(nameof(AppliedPrice), AppliedPriceses.Close)
		.SetDisplay("Applied Price", "Price input for the moving average", "Moving Average");

		_reverseSignal = Param(nameof(ReverseSignal), false)
		.SetDisplay("Reverse Signals", "Invert MA and price relationship", "Signals");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for market data", "General");
	}

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

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

		_ma = null;
		_maHistory.Clear();
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_priceStep = 0m;
	}

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

		_priceStep = Security?.PriceStep ?? 1m;
		_ma = CreateMovingAverage(MaMethod, MaPeriod);

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

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

	}

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

		var price = GetPrice(candle);
		var maResult = _ma!.Process(new DecimalIndicatorValue(_ma, price, candle.OpenTime) { IsFinal = true });

		if (maResult.IsEmpty || !_ma.IsFormed)
			return;

		var maValue = maResult.ToDecimal();

		_maHistory.Add(maValue);

		var shift = Math.Max(0, MaShift);
		var historySize = shift + 2;
		if (_maHistory.Count > historySize)
		_maHistory.RemoveRange(0, _maHistory.Count - historySize);

		var targetBack = shift + 1;
		if (_maHistory.Count <= targetBack)
		return;

		var maIndex = _maHistory.Count - targetBack - 1;
		var shiftedMa = _maHistory[maIndex];

		var distance = Math.Max(0, DistancePoints) * _priceStep;

		if (Position > 0)
		{
			// Manage long position risk before looking for new entries.
			if (TryCloseLong(candle))
			ResetRiskLevels();

			return;
		}

		if (Position < 0)
		{
			// Manage short position risk before looking for new entries.
			if (TryCloseShort(candle))
			ResetRiskLevels();

			return;
		}

		// Guard against disabled sides.
		if (!EnableBuy && !EnableSell)
		return;

		var threshold = (int)Combination;
		var orderVolume = TradeVolume;

		// Determine trading direction based on moving average placement.
		var allowBuy = EnableBuy && ((!ReverseSignal && shiftedMa > price + distance) || (ReverseSignal && shiftedMa < price - distance));
		var allowSell = EnableSell && ((!ReverseSignal && shiftedMa < price - distance) || (ReverseSignal && shiftedMa > price + distance));

		if (!allowBuy && !allowSell)
		return;

		var stopPoints = Math.Max(0, StopLossPoints);
		var takePoints = Math.Max(0, TakeProfitPoints);

		var executed = false;

		if (allowBuy)
		{
			if (PassesProbabilityGate(candle, true, threshold))
			{
				// Close opposite short if needed and open a new long position.
				var volume = orderVolume + Math.Abs(Position);
				BuyMarket(volume);

				var entryPrice = candle.ClosePrice;
				_stopLossPrice = stopPoints > 0 ? entryPrice - stopPoints * _priceStep : null;
				_takeProfitPrice = takePoints > 0 ? entryPrice + takePoints * _priceStep : null;

				executed = true;
			}
		}

		if (!executed && allowSell)
		{
			if (PassesProbabilityGate(candle, false, threshold))
			{
				// Close opposite long if needed and open a new short position.
				var volume = orderVolume + Math.Abs(Position);
				SellMarket(volume);

				var entryPrice = candle.ClosePrice;
				_stopLossPrice = stopPoints > 0 ? entryPrice + stopPoints * _priceStep : null;
				_takeProfitPrice = takePoints > 0 ? entryPrice - takePoints * _priceStep : null;
			}
		}
	}

	private static bool PassesProbabilityGate(ICandleMessage candle, bool isBuy, int threshold)
	{
		var randomValue = HashCode.Combine(candle.OpenTime.Ticks, candle.ClosePrice, candle.TotalVolume, isBuy) & 0x7FFF;
		return randomValue < threshold;
	}

	private bool TryCloseLong(ICandleMessage candle)
	{
		var closed = false;

		if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
		{
			if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			closed = true;
		}
		else if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
		{
			if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			closed = true;
		}

		return closed;
	}

	private bool TryCloseShort(ICandleMessage candle)
	{
		var closed = false;

		if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
		{
			if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			closed = true;
		}
		else if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
		{
			if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			closed = true;
		}

		return closed;
	}

	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private decimal GetPrice(ICandleMessage candle)
	{
		return AppliedPrice switch
		{
			AppliedPriceses.Close => candle.ClosePrice,
			AppliedPriceses.Open => candle.OpenPrice,
			AppliedPriceses.High => candle.HighPrice,
			AppliedPriceses.Low => candle.LowPrice,
			AppliedPriceses.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceses.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceses.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
	{
		return method switch
		{
			MovingAverageMethods.Sma => new SimpleMovingAverage { Length = period },
			MovingAverageMethods.Ema => new ExponentialMovingAverage { Length = period },
			MovingAverageMethods.Smma => new SmoothedMovingAverage { Length = period },
			MovingAverageMethods.Lwma => new WeightedMovingAverage { Length = period },
			_ => new SimpleMovingAverage { Length = period }
		};
	}
}