Ver no GitHub

Estratégia Poker Show

Visão geral

A estratégia Poker Show é uma portação direta do assessor especialista do MetaTrader 5 "Poker_SHOW". Combina um filtro de tendência de média móvel com um gatilho probabilístico que imita o sorteio de uma mão de pôquer. As operações são executadas apenas quando o valor da mão gerada aleatoriamente cai abaixo de um limite configurável de combinação de pôquer. A abordagem produz entradas pouco frequentes enquanto permanece alinhada com a tendência predominante detectada pela média móvel.

A estratégia trabalha em um único símbolo e depende de velas regulares baseadas em tempo. As decisões de trading são avaliadas uma vez por vela completada, o que corresponde ao assessor original que reage na abertura de cada nova barra.

Lógica principal

  1. Filtro de tendência de média móvel

    • Uma média móvel configurável (SMA, EMA, SMMA ou LWMA) é calculada a partir da fonte de preço selecionada (fechamento, abertura, máxima, mínima, mediana, típico ou preço ponderado).
    • O indicador pode ser deslocado para frente no tempo para reproduzir o input "shift" do MetaTrader. A estratégia sempre usa o valor da última vela completamente formada, igual ao EA fonte.
  2. Portão de probabilidade

    • Cada lado (comprado ou vendido) sorteia um valor aleatório independente entre 0 e 32.767 em cada barra.
    • O sorteio é comparado com a combinação de pôquer selecionada. Combinações de rank mais alto (p. ex., royal flush) têm limites numéricos menores e portanto se acionam com menos frequência, enquanto combinações de rank menor (p. ex., um par) operam com mais frequência.
  3. Regras direcionais

    • Operações compradas exigem que a média móvel permaneça acima do preço pelo menos a distância configurada. Quando a opção Sinais invertidos está ativa, a condição é invertida.
    • Operações vendidas exigem que a média móvel permaneça abaixo do preço com a mesma margem, com a condição invertida quando o interruptor de inversão está ativo.
    • Apenas uma posição pode estar ativa de cada vez. Entrar na direção oposta compensa automaticamente qualquer exposição aberta antes de estabelecer o novo trade.
  4. Gestão de risco

    • Os níveis opcionais de stop loss e take profit são calculados em passos de preço (pontos) relativos ao preço de execução. Definir uma distância como zero desabilita o nível correspondente.
    • Stops e alvos são verificados em cada vela completada. Quando atingidos, a estratégia fecha a posição e redefine os marcadores de risco.
  5. Proteção de posição

    • O módulo de proteção integrado do StockSharp é ativado na inicialização para preservar a conta de perdas inesperadas durante execuções manuais.

Parâmetros

Parâmetro Descrição
Combinação de pôquer Limite de probabilidade que o sorteio aleatório deve exceder para permitir um novo trade. Representa mãos clássicas de pôquer, de royal flush (mais rara) a um par (mais comum).
Volume Volume de ordem em lotes. Usado tanto para novas entradas quanto para reverter posições existentes.
Stop loss Distância entre o preço de entrada e o stop protetor, medida em passos de preço. Definir como zero para desabilitar.
Take profit Distância entre o preço de entrada e o alvo de lucro, medida em passos de preço. Definir como zero para desabilitar.
Habilitar compra Permite à estratégia abrir posições compradas.
Habilitar venda Permite à estratégia abrir posições vendidas.
Distância MA Distância mínima em passos de preço entre o valor da média móvel e o preço atual. Atua como filtro de confirmação de tendência.
Período MA Número de barras usadas pela média móvel.
Deslocamento MA Deslocamento horizontal aplicado à média móvel (em barras), correspondendo ao input ma_shift do MetaTrader.
Método MA Tipo de suavização da média móvel: simples, exponencial, suavizada ou ponderada linear.
Preço aplicado Preço de vela utilizado no cálculo da média móvel.
Sinais invertidos Inverte a comparação entre a média móvel e o preço, trocando efetivamente a lógica de comprado e vendido.
Tipo de vela Período de tempo da assinatura de velas. O padrão é uma hora para replicar as configurações originais.

Notas e recomendações

  • O portão de probabilidade torna a estratégia altamente estocástica. Os backtests devem usar múltiplas execuções ou análise de Monte Carlo para entender a distribuição dos resultados.
  • Como o gerenciamento de operações depende de velas completadas, picos intrabarra grandes podem ultrapassar os níveis de stop ou alvo antes que a estratégia possa reagir. Considere executar em períodos de tempo menores se esse comportamento for indesejável.
  • Para reproduzir fielmente o ambiente do MetaTrader, certifique-se de que o instrumento usa o mesmo tamanho de contrato e passo de preço para que as distâncias baseadas em pontos correspondam aos lotes e valores de pip originais.
  • A estratégia usa ordens a mercado (BuyMarket e SellMarket) como no assessor especialista fonte. O tratamento de slippage é delegado à infraestrutura de execução do 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 }
		};
	}
}