View on GitHub

Poker Show Strategy

Overview

The Poker Show strategy is a direct port of the MetaTrader 5 expert advisor "Poker_SHOW". It combines a moving average trend filter with a probabilistic trigger that mimics drawing a poker hand. Trades are executed only when the randomly generated hand value falls below a configurable poker combination threshold. The approach produces infrequent entries while staying aligned with the prevailing trend detected by the moving average.

The strategy works on a single symbol and relies on regular time-based candles. Trading decisions are evaluated once per completed candle, which matches the original advisor that reacts on the opening of each new bar.

Core Logic

  1. Moving Average Trend Filter

    • A configurable moving average (SMA, EMA, SMMA, or LWMA) is calculated from the selected price source (close, open, high, low, median, typical, or weighted price).
    • The indicator can be shifted forward in time to reproduce the MetaTrader "shift" input. The strategy always uses the value from the previous fully formed candle, just like the source EA.
  2. Probability Gate

    • Each side (long or short) draws an independent random value between 0 and 32,767 on every bar.
    • The draw is compared with the selected poker combination. Higher-ranked combinations (e.g., straight flush) have smaller numeric thresholds and therefore trigger less frequently, while lower-ranked combinations (e.g., one pair) trade more often.
  3. Directional Rules

    • Long trades require the moving average to stay above the price by at least the configured distance. When the Reverse Signals option is enabled, the condition is inverted.
    • Short trades require the moving average to stay below the price by the same margin, with the condition inverted when the reverse switch is active.
    • Only one position can be active at a time. Entering in the opposite direction automatically offsets any open exposure before establishing the new trade.
  4. Risk Management

    • Optional stop loss and take profit levels are calculated in price steps (points) relative to the execution price. Setting a distance to zero disables the corresponding level.
    • Stops and targets are checked on every completed candle. When hit, the strategy closes the position and resets risk markers.
  5. Position Protection

    • The built-in StockSharp protection module is activated on start to preserve the account from unexpected losses during manual runs.

Parameters

Parameter Description
Poker Combination Probability threshold that must exceed the random draw to allow a new trade. Represents classic poker hands from straight flush (rarest) to one pair (most common).
Volume Order volume in lots. Used both for fresh entries and for reversing existing positions.
Stop Loss Distance between the entry price and the protective stop, measured in price steps. Set to zero to disable.
Take Profit Distance between the entry price and the profit target, measured in price steps. Set to zero to disable.
Enable Buy Allows the strategy to open long positions.
Enable Sell Allows the strategy to open short positions.
MA Distance Minimum distance in price steps between the moving average value and the current price. Acts as a trend confirmation filter.
MA Period Number of bars used by the moving average.
MA Shift Horizontal shift applied to the moving average (in bars), matching the MetaTrader ma_shift input.
MA Method Moving average smoothing type: simple, exponential, smoothed, or linear weighted.
Applied Price Candle price used in the moving average calculation.
Reverse Signals Inverts the comparison between the moving average and price, effectively swapping long and short logic.
Candle Type Time frame of the candle subscription. Default is one hour to replicate the original settings.

Notes and Recommendations

  • The probability gate makes the strategy highly stochastic. Backtests should use multiple runs or Monte Carlo analysis to understand the distribution of outcomes.
  • Because trade management relies on completed candles, large intrabar spikes may overshoot stop or target levels before the strategy can react. Consider running on lower time frames if this behavior is undesirable.
  • To reproduce the MetaTrader environment faithfully, ensure the instrument uses the same contract size and price step so that point-based distances match the original lots and pip values.
  • The strategy uses market orders (BuyMarket and SellMarket) as in the source expert advisor. Slippage handling is delegated to the StockSharp execution infrastructure.
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 }
		};
	}
}