Ver en GitHub

Estrategia Poker Show

Descripción general

La estrategia Poker Show es un porteo directo del asesor experto de MetaTrader 5 "Poker_SHOW". Combina un filtro de tendencia de media móvil con un disparador probabilístico que imita el reparto de una mano de póker. Las operaciones se ejecutan solo cuando el valor de la mano generada aleatoriamente cae por debajo de un umbral configurable de combinación de póker. El enfoque produce entradas poco frecuentes mientras permanece alineado con la tendencia predominante detectada por la media móvil.

La estrategia trabaja en un único símbolo y se basa en velas basadas en tiempo regular. Las decisiones de trading se evalúan una vez por vela completada, lo que coincide con el asesor original que reacciona en la apertura de cada nueva barra.

Lógica principal

  1. Filtro de tendencia de media móvil

    • Se calcula una media móvil configurable (SMA, EMA, SMMA o LWMA) a partir de la fuente de precio seleccionada (cierre, apertura, máximo, mínimo, mediana, típico o precio ponderado).
    • El indicador puede desplazarse hacia adelante en el tiempo para reproducir el input "shift" de MetaTrader. La estrategia siempre usa el valor de la última vela completamente formada, igual que el EA fuente.
  2. Puerta de probabilidad

    • Cada lado (largo o corto) extrae un valor aleatorio independiente entre 0 y 32.767 en cada barra.
    • El sorteo se compara con la combinación de póker seleccionada. Las combinaciones de rango más alto (p. ej., escalera de color) tienen umbrales numéricos más pequeños y por lo tanto se activan con menor frecuencia, mientras que las de rango inferior (p. ej., una pareja) operan con más frecuencia.
  3. Reglas direccionales

    • Las operaciones largas requieren que la media móvil se mantenga por encima del precio al menos la distancia configurada. Cuando la opción Señales invertidas está activada, la condición se invierte.
    • Las operaciones cortas requieren que la media móvil se mantenga por debajo del precio con el mismo margen, con la condición invertida cuando el interruptor de inversión está activo.
    • Solo puede haber una posición activa a la vez. Entrar en la dirección opuesta compensa automáticamente cualquier exposición abierta antes de establecer la nueva operación.
  4. Gestión de riesgo

    • Los niveles opcionales de stop loss y take profit se calculan en pasos de precio (puntos) relativos al precio de ejecución. Establecer una distancia en cero deshabilita el nivel correspondiente.
    • Los stops y objetivos se verifican en cada vela completada. Cuando se alcanzan, la estrategia cierra la posición y restablece los marcadores de riesgo.
  5. Protección de posición

    • El módulo de protección integrado de StockSharp se activa al inicio para preservar la cuenta de pérdidas inesperadas durante ejecuciones manuales.

Parámetros

Parámetro Descripción
Combinación de póker Umbral de probabilidad que debe superar el sorteo aleatorio para permitir un nuevo trade. Representa manos clásicas de póker, desde escalera de color (más rara) hasta una pareja (más común).
Volumen Volumen de orden en lotes. Usado tanto para entradas nuevas como para invertir posiciones existentes.
Stop loss Distancia entre el precio de entrada y el stop protector, medida en pasos de precio. Poner en cero para deshabilitar.
Take profit Distancia entre el precio de entrada y el objetivo de beneficio, medida en pasos de precio. Poner en cero para deshabilitar.
Habilitar compra Permite a la estrategia abrir posiciones largas.
Habilitar venta Permite a la estrategia abrir posiciones cortas.
Distancia MA Distancia mínima en pasos de precio entre el valor de la media móvil y el precio actual. Actúa como filtro de confirmación de tendencia.
Período MA Número de barras usadas por la media móvil.
Desplazamiento MA Desplazamiento horizontal aplicado a la media móvil (en barras), coincidiendo con el input ma_shift de MetaTrader.
Método MA Tipo de suavizado de la media móvil: simple, exponencial, suavizada o ponderada lineal.
Precio aplicado Precio de vela utilizado en el cálculo de la media móvil.
Señales invertidas Invierte la comparación entre la media móvil y el precio, intercambiando efectivamente la lógica de largo y corto.
Tipo de vela Marco temporal de la suscripción de velas. El predeterminado es una hora para replicar la configuración original.

Notas y recomendaciones

  • La puerta de probabilidad hace que la estrategia sea altamente estocástica. Los backtests deben usar múltiples ejecuciones o análisis de Monte Carlo para comprender la distribución de los resultados.
  • Debido a que la gestión de operaciones depende de velas completadas, los picos intrabarra grandes pueden sobrepasar los niveles de stop o objetivo antes de que la estrategia pueda reaccionar. Considere ejecutar en marcos temporales más bajos si este comportamiento no es deseable.
  • Para reproducir fielmente el entorno de MetaTrader, asegúrese de que el instrumento use el mismo tamaño de contrato y paso de precio para que las distancias basadas en puntos coincidan con los lotes y valores de pip originales.
  • La estrategia utiliza órdenes de mercado (BuyMarket y SellMarket) como en el asesor experto fuente. El manejo del deslizamiento se delega a la infraestructura de ejecución de 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 }
		};
	}
}