在 GitHub 上查看

Poker Show 策略

概述

Poker Show 策略是 MetaTrader 5 指标交易系统 “Poker_SHOW” 的完整移植版本。策略将趋势型移动平均线过滤器与概率触发器结合起来,通过模拟抽取扑克手牌来控制入场频率。只有当随机生成的数值低于所选扑克组合阈值时才会开仓,因此策略在保持顺势交易的同时,交易次数较少。

该策略面向单一标的,使用常规的时间周期蜡烛图。在每根蜡烛完成时重新评估交易信号,与原始 EA 在新 K 线打开时做出决策的逻辑一致。

核心逻辑

  1. 移动平均趋势过滤

    • 可配置的移动平均线(SMA、EMA、SMMA 或 LWMA)基于指定的价格类型(收盘价、开盘价、最高价、最低价、中价、典型价、加权价)进行计算。
    • 指标可以向前平移若干根柱线,从而复刻 MetaTrader 中的 ma_shift 参数。策略始终使用上一根完整蜡烛的移动平均值。
  2. 概率触发器

    • 多头与空头方向在每个周期各自生成一个 0 到 32,767 的随机整数。
    • 随机值与所选扑克组合进行比较。级别越高的组合(如同花顺)对应的阈值越小,触发频率越低;级别越低的组合(如一对)则会更频繁地发出信号。
  3. 方向性规则

    • 当移动平均线高于当前价格且差距超过设定的点数时触发做多。如果启用了 Reverse Signals 选项,则条件反向。
    • 当移动平均线低于当前价格且差距超过设定的点数时触发做空。启用反向信号后逻辑同样翻转。
    • 任意时刻只允许持有一个净头寸。若方向相反的信号触发,将先平掉现有仓位,再建立新的反向仓位。
  4. 风险控制

    • 止损与止盈均以价格步长(点数)为单位,相对于成交价计算。设置为 0 即表示禁用。
    • 每根蜡烛收盘时都会检查是否触发止损或止盈。一旦价格触碰目标,策略立即平仓并清除内部风险标记。
  5. 仓位保护

    • 策略启动时会调用 StockSharp 的保护模块,以便在手动运行时限制极端亏损。

参数说明

参数 说明
Poker Combination 允许开仓的概率阈值,模拟从同花顺到一对的经典扑克组合。阈值越小,信号越稀少。
Volume 订单手数,用于新开仓以及反向开仓时平旧仓。
Stop Loss 距离入场价的止损距离(点数)。为 0 时不设置止损。
Take Profit 距离入场价的止盈距离(点数)。为 0 时不设置止盈。
Enable Buy 允许策略开多仓。
Enable Sell 允许策略开空仓。
MA Distance 移动平均值与价格之间的最小距离(点数),用于确认趋势。
MA Period 移动平均线的计算周期。
MA Shift 移动平均线的水平平移(单位:柱),对应 MetaTrader 的 ma_shift 设置。
MA Method 移动平均线类型:简单、指数、平滑或线性加权。
Applied Price 参与移动平均计算的蜡烛价格类型。
Reverse Signals 反转移动平均与价格的比较关系,从而互换多空逻辑。
Candle Type 使用的蜡烛周期,默认是一小时,与原策略保持一致。

使用建议

  • 概率门控使策略具有明显的随机性。回测时建议进行多次重复或使用蒙特卡洛分析来观察结果分布。
  • 因为止损和止盈只在蜡烛收盘时检查,盘中快速波动可能会在策略反应之前突破目标位。若担心该问题,可选择更短周期的蜡烛。
  • 为尽量还原 MetaTrader 环境,应确认标的的合约规模和最小变动价位与原始设置一致。
  • 策略通过 BuyMarketSellMarket 提交市价单,与原始 EA 的执行方式一致。具体的滑点处理由 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 }
		};
	}
}