Poker Show 策略
概述
Poker Show 策略是 MetaTrader 5 指标交易系统 “Poker_SHOW” 的完整移植版本。策略将趋势型移动平均线过滤器与概率触发器结合起来,通过模拟抽取扑克手牌来控制入场频率。只有当随机生成的数值低于所选扑克组合阈值时才会开仓,因此策略在保持顺势交易的同时,交易次数较少。
该策略面向单一标的,使用常规的时间周期蜡烛图。在每根蜡烛完成时重新评估交易信号,与原始 EA 在新 K 线打开时做出决策的逻辑一致。
核心逻辑
移动平均趋势过滤
- 可配置的移动平均线(SMA、EMA、SMMA 或 LWMA)基于指定的价格类型(收盘价、开盘价、最高价、最低价、中价、典型价、加权价)进行计算。
- 指标可以向前平移若干根柱线,从而复刻 MetaTrader 中的
ma_shift参数。策略始终使用上一根完整蜡烛的移动平均值。
概率触发器
- 多头与空头方向在每个周期各自生成一个 0 到 32,767 的随机整数。
- 随机值与所选扑克组合进行比较。级别越高的组合(如同花顺)对应的阈值越小,触发频率越低;级别越低的组合(如一对)则会更频繁地发出信号。
方向性规则
- 当移动平均线高于当前价格且差距超过设定的点数时触发做多。如果启用了 Reverse Signals 选项,则条件反向。
- 当移动平均线低于当前价格且差距超过设定的点数时触发做空。启用反向信号后逻辑同样翻转。
- 任意时刻只允许持有一个净头寸。若方向相反的信号触发,将先平掉现有仓位,再建立新的反向仓位。
风险控制
- 止损与止盈均以价格步长(点数)为单位,相对于成交价计算。设置为 0 即表示禁用。
- 每根蜡烛收盘时都会检查是否触发止损或止盈。一旦价格触碰目标,策略立即平仓并清除内部风险标记。
仓位保护
- 策略启动时会调用 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 环境,应确认标的的合约规模和最小变动价位与原始设置一致。
- 策略通过
BuyMarket与SellMarket提交市价单,与原始 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 }
};
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import (ExponentialMovingAverage, SimpleMovingAverage,
SmoothedMovingAverage, WeightedMovingAverage)
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math, Decimal
from indicator_extensions import *
class poker_show_strategy(Strategy):
def __init__(self):
super(poker_show_strategy, self).__init__()
self._combination = self.Param("Combination", 16383)
self._stop_loss_points = self.Param("StopLossPoints", 50)
self._take_profit_points = self.Param("TakeProfitPoints", 150)
self._ma_period = self.Param("MaPeriod", 24)
self._ma_shift = self.Param("MaShift", 0)
self._reverse_signal = self.Param("ReverseSignal", False)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._ma = None
self._ma_history = []
self._stop_loss_price = None
self._take_profit_price = None
self._price_step = 1.0
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(poker_show_strategy, self).OnStarted2(time)
self._price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self._price_step <= 0:
self._price_step = 1.0
self._ma = ExponentialMovingAverage()
self._ma.Length = self._ma_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
ma_result = process_float(self._ma, Decimal(float(price)), candle.OpenTime, True)
if ma_result.IsEmpty or not self._ma.IsFormed:
return
ma_value = float(ma_result.Value)
self._ma_history.append(ma_value)
shift = max(0, self._ma_shift.Value)
history_size = shift + 2
if len(self._ma_history) > history_size:
self._ma_history = self._ma_history[len(self._ma_history) - history_size:]
target_back = shift + 1
if len(self._ma_history) <= target_back:
return
ma_index = len(self._ma_history) - target_back - 1
shifted_ma = self._ma_history[ma_index]
distance = max(0, self._stop_loss_points.Value) * self._price_step * 0
if self.Position > 0:
if self._try_close_long(candle):
self._reset_risk_levels()
return
if self.Position < 0:
if self._try_close_short(candle):
self._reset_risk_levels()
return
threshold = self._combination.Value
reverse = self._reverse_signal.Value
allow_buy = (not reverse and shifted_ma > price) or (reverse and shifted_ma < price)
allow_sell = (not reverse and shifted_ma < price) or (reverse and shifted_ma > price)
if not allow_buy and not allow_sell:
return
stop_points = max(0, self._stop_loss_points.Value)
take_points = max(0, self._take_profit_points.Value)
executed = False
if allow_buy:
if self._passes_probability_gate(candle, True, threshold):
volume = float(self.Volume) + abs(self.Position)
self.BuyMarket(volume)
entry_price = float(candle.ClosePrice)
self._stop_loss_price = entry_price - stop_points * self._price_step if stop_points > 0 else None
self._take_profit_price = entry_price + take_points * self._price_step if take_points > 0 else None
executed = True
if not executed and allow_sell:
if self._passes_probability_gate(candle, False, threshold):
volume = float(self.Volume) + abs(self.Position)
self.SellMarket(volume)
entry_price = float(candle.ClosePrice)
self._stop_loss_price = entry_price + stop_points * self._price_step if stop_points > 0 else None
self._take_profit_price = entry_price - take_points * self._price_step if take_points > 0 else None
def _passes_probability_gate(self, candle, is_buy, threshold):
ticks = candle.OpenTime.Ticks if hasattr(candle.OpenTime, 'Ticks') else 0
close_val = int(float(candle.ClosePrice) * 10000) & 0x7FFF
vol_val = int(float(candle.TotalVolume)) & 0x7FFF if candle.TotalVolume is not None else 0
buy_val = 1 if is_buy else 0
random_value = (ticks ^ close_val ^ vol_val ^ buy_val) & 0x7FFF
return random_value < threshold
def _try_close_long(self, candle):
if self._stop_loss_price is not None and float(candle.LowPrice) <= self._stop_loss_price:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
return True
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
return True
return False
def _try_close_short(self, candle):
if self._stop_loss_price is not None and float(candle.HighPrice) >= self._stop_loss_price:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
return True
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
return True
return False
def _reset_risk_levels(self):
self._stop_loss_price = None
self._take_profit_price = None
def OnReseted(self):
super(poker_show_strategy, self).OnReseted()
self._ma = None
self._ma_history = []
self._stop_loss_price = None
self._take_profit_price = None
self._price_step = 0.0
def CreateClone(self):
return poker_show_strategy()