Ver no GitHub

Estratégia de sorteio aleatório de máquina de pinball

Visão geral

Esta estratégia é uma conversão StockSharp direta do MetaTrader 4 consultor especialista Pinball_machine.mq4. O robô original desenhou inteiros aleatórios em cada tick recebido e abria uma ordem de mercado sempre que dois desses sorteios correspondiam. A versão StockSharp preserva o mesmo comportamento de loteria: em cada vela finalizada do intervalo de tempo selecionado, o algoritmo executa dois pares de sorteia aleatoriamente e entra em uma posição de mercado longa ou curta quando o par correspondente contém valores iguais. Stop-loss e take-profit as distâncias também são aleatórias em cada avaliação, reproduzindo a sensação da rotina original de "pinball", onde as negociações saltam e fora imprevisível.

Lógica de negociação

  • Assine as velas definidas pelo parâmetro CandleType e aguarde as barras totalmente formadas.
  • Para cada vela finalizada gere quatro inteiros distribuídos uniformemente em [0, RandomMaxValue]. O primeiro par pertence ao potencial entrada longa, o segundo par pertence à potencial entrada curta.
  • Desenhe dois inteiros adicionais entre MinStopLossPoints/MaxStopLossPoints e MinTakeProfitPoints/MaxTakeProfitPoints para determinar as distâncias de proteção (expressas em etapas de preços) compartilhadas por ambos os lados da avaliação.
  • Se o primeiro e o segundo números inteiros aleatórios corresponderem, envie uma ordem de compra a mercado com volume TradeVolume. Se o terceiro e o quarto valores coincidem, envie uma ordem de venda a mercado com o mesmo volume. Ambas as condições podem disparar dentro da mesma vela, exatamente como em a versão MQL onde as ordens de compra e venda eram eventos independentes.
  • Anexe imediatamente uma ordem de stop-loss e de take-profit (se a distância desenhada for maior que zero). As distâncias são interpretadas como múltiplos do PriceStep do instrumento, espelhando o multiplicador Point usado em MetaTrader.

Gerenciamento de pedidos e controles de risco

  • StartProtection() é invocado quando a estratégia é iniciada para que StockSharp gerencie ordens de proteção em nome da estratégia.
  • Cada entrada mede a posição resultante (Position ± TradeVolume) e a passa para SetStopLoss e SetTakeProfit, que permite que a plataforma consolide ordens de proteção mesmo quando várias negociações estão em execução ao mesmo tempo.
  • Se os parâmetros de distância mínima ou máxima forem definidos como zero ou um número negativo, a proteção correspondente é pulou para esse ciclo.

Parâmetros

Parâmetro Descrição
TradeVolume Tamanho do pedido em lotes/contratos enviados para cada entrada aleatória.
CandleType Prazo das velas que acionam os sorteios aleatórios. Períodos mais curtos emulam o EA original baseado em ticks mais de perto.
RandomMaxValue Limite superior inclusivo para sorteios de inteiros. Um valor maior diminui a probabilidade de correspondência de números e, portanto, reduz a frequência de negociação.
MinStopLossPoints Limite inferior (em etapas de preço) para a distância de stop-loss gerada aleatoriamente.
MaxStopLossPoints Limite superior (em etapas de preço) para a distância do stop loss.
MinTakeProfitPoints Limite inferior (em etapas de preço) para a distância de lucro gerada aleatoriamente.
MaxTakeProfitPoints Limite superior (em etapas de preço) para a distância de realização do lucro.
RandomSeed Semente do gerador de números pseudo-aleatórios. Zero mantém o comportamento baseado no tempo; qualquer outro valor torna a sequência reproduzível.

Notas de implementação

  • O script MetaTrader foi orientado por ticks; a porta StockSharp usa conclusões de vela porque o API de alto nível opera em eventos de série temporal. Definir um CandleType muito curto (por exemplo, velas de um segundo ou de tick) restaura a natureza acelerada do original.
  • Os valores de stop-loss e take-profit são gerados uma vez por avaliação e reutilizados para as ramificações longas e curtas, exatamente como na fonte EA.
  • Certifique-se de que o instrumento negociado expõe um PriceStep válido, caso contrário, as distâncias de proteção expressas em pontos podem precisar de ajuste manual.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Lottery-style strategy that makes two independent random entry decisions per candle.
/// Each entry receives random stop-loss and take-profit distances measured in price steps.
/// </summary>
public class PinballMachineRandomDrawStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _randomMaxValue;
	private readonly StrategyParam<int> _minStopLossPoints;
	private readonly StrategyParam<int> _maxStopLossPoints;
	private readonly StrategyParam<int> _minTakeProfitPoints;
	private readonly StrategyParam<int> _maxTakeProfitPoints;
	private readonly StrategyParam<int> _randomSeed;

	private Random _random;
	private decimal _protectedPosition;
	private ProtectedPosition _protection;

	public PinballMachineRandomDrawStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume submitted for every matching pair.", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candles that trigger random draws.", "General");

		_randomMaxValue = Param(nameof(RandomMaxValue), 99)
			.SetRange(0, 1_000_000)
			.SetDisplay("Random Maximum", "Inclusive upper bound for entry draws.", "Random");

		_minStopLossPoints = Param(nameof(MinStopLossPoints), 10)
			.SetDisplay("Minimum Stop Loss", "Minimum random stop-loss distance in price steps.", "Protection");

		_maxStopLossPoints = Param(nameof(MaxStopLossPoints), 50)
			.SetDisplay("Maximum Stop Loss", "Maximum random stop-loss distance in price steps.", "Protection");

		_minTakeProfitPoints = Param(nameof(MinTakeProfitPoints), 10)
			.SetDisplay("Minimum Take Profit", "Minimum random take-profit distance in price steps.", "Protection");

		_maxTakeProfitPoints = Param(nameof(MaxTakeProfitPoints), 100)
			.SetDisplay("Maximum Take Profit", "Maximum random take-profit distance in price steps.", "Protection");

		_randomSeed = Param(nameof(RandomSeed), 0)
			.SetDisplay("Random Seed", "Zero uses a time-based sequence; any other value is reproducible.", "Random");
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int RandomMaxValue
	{
		get => _randomMaxValue.Value;
		set => _randomMaxValue.Value = value;
	}

	public int MinStopLossPoints
	{
		get => _minStopLossPoints.Value;
		set => _minStopLossPoints.Value = value;
	}

	public int MaxStopLossPoints
	{
		get => _maxStopLossPoints.Value;
		set => _maxStopLossPoints.Value = value;
	}

	public int MinTakeProfitPoints
	{
		get => _minTakeProfitPoints.Value;
		set => _minTakeProfitPoints.Value = value;
	}

	public int MaxTakeProfitPoints
	{
		get => _maxTakeProfitPoints.Value;
		set => _maxTakeProfitPoints.Value = value;
	}

	public int RandomSeed
	{
		get => _randomSeed.Value;
		set => _randomSeed.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_random = null;
		_protectedPosition = 0m;
		_protection = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_random = RandomSeed == 0 ? new Random() : new Random(RandomSeed);
		StartProtectionBook();

		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;

		ProcessProtection(candle);

		var draw = DrawEvaluation(
			_random,
			RandomMaxValue,
			MinStopLossPoints,
			MaxStopLossPoints,
			MinTakeProfitPoints,
			MaxTakeProfitPoints);

		if (draw.EnterLong)
			SubmitEntry(Sides.Buy, candle, draw.StopLossPoints, draw.TakeProfitPoints);

		if (draw.EnterShort)
			SubmitEntry(Sides.Sell, candle, draw.StopLossPoints, draw.TakeProfitPoints);
	}

	private void SubmitEntry(Sides side, ICandleMessage candle, int stopLossPoints, int takeProfitPoints)
	{
		if (TradeVolume <= 0m)
			return;

		SubmitMarket(side, TradeVolume, "Pinball entry");
		_protectedPosition += side == Sides.Buy ? TradeVolume : -TradeVolume;

		if (_protectedPosition == 0m)
		{
			_protection = null;
			return;
		}

		var priceStep = Security?.PriceStep;
		if (priceStep is not decimal step || step <= 0m || (stopLossPoints <= 0 && takeProfitPoints <= 0))
		{
			_protection = null;
			return;
		}

		var protection = new ProtectedPosition(
			_protectedPosition > 0m ? Sides.Buy : Sides.Sell,
			Math.Abs(_protectedPosition),
			candle.CloseTime);
		SetStopLoss(protection, candle.ClosePrice, stopLossPoints, step);
		SetTakeProfit(protection, candle.ClosePrice, takeProfitPoints, step);
		_protection = protection;
	}

	private void ProcessProtection(ICandleMessage candle)
	{
		var protection = _protection;
		if (protection is null || candle.CloseTime <= protection.ActiveAfter)
			return;

		var stopHit = protection.StopLossPrice is decimal stop &&
			(protection.Side == Sides.Buy ? candle.LowPrice <= stop : candle.HighPrice >= stop);
		var takeHit = !stopHit && protection.TakeProfitPrice is decimal take &&
			(protection.Side == Sides.Buy ? candle.HighPrice >= take : candle.LowPrice <= take);

		if (!stopHit && !takeHit)
			return;

		var exitSide = protection.Side == Sides.Buy ? Sides.Sell : Sides.Buy;
		SubmitMarket(exitSide, protection.Volume, "Pinball protection exit");
		_protectedPosition = 0m;
		_protection = null;
	}

	private void SubmitMarket(Sides side, decimal volume, string comment)
	{
		var order = CreateOrder(side, 0m, volume);
		order.Comment = comment;
		RegisterOrder(order);
	}

	private void StartProtectionBook()
	{
		_protectedPosition = Position;
		_protection = null;
	}

	private static void SetStopLoss(ProtectedPosition protection, decimal entryPrice, int points, decimal priceStep)
	{
		if (points <= 0)
			return;

		var distance = points * priceStep;
		protection.StopLossPrice = protection.Side == Sides.Buy
			? entryPrice - distance
			: entryPrice + distance;
	}

	private static void SetTakeProfit(ProtectedPosition protection, decimal entryPrice, int points, decimal priceStep)
	{
		if (points <= 0)
			return;

		var distance = points * priceStep;
		protection.TakeProfitPrice = protection.Side == Sides.Buy
			? entryPrice + distance
			: entryPrice - distance;
	}

	internal static Evaluation DrawEvaluation(
		Random random,
		int randomMaxValue,
		int minStopLossPoints,
		int maxStopLossPoints,
		int minTakeProfitPoints,
		int maxTakeProfitPoints)
	{
		ArgumentNullException.ThrowIfNull(random);

		if (randomMaxValue < 0)
			throw new ArgumentOutOfRangeException(nameof(randomMaxValue));

		int drawEntry() => randomMaxValue == int.MaxValue
			? (int)random.NextInt64((long)int.MaxValue + 1L)
			: random.Next(randomMaxValue + 1);

		return new(
			drawEntry(),
			drawEntry(),
			drawEntry(),
			drawEntry(),
			DrawDistance(random, minStopLossPoints, maxStopLossPoints),
			DrawDistance(random, minTakeProfitPoints, maxTakeProfitPoints));
	}

	private static int DrawDistance(Random random, int minimum, int maximum)
	{
		if (minimum <= 0 || maximum <= 0 || minimum > maximum)
			return 0;

		return maximum == int.MaxValue
			? (int)random.NextInt64(minimum, (long)maximum + 1L)
			: random.Next(minimum, maximum + 1);
	}

	internal readonly record struct Evaluation(
		int LongFirst,
		int LongSecond,
		int ShortFirst,
		int ShortSecond,
		int StopLossPoints,
		int TakeProfitPoints)
	{
		public bool EnterLong => LongFirst == LongSecond;
		public bool EnterShort => ShortFirst == ShortSecond;
	}

	private sealed class ProtectedPosition(Sides side, decimal volume, DateTime activeAfter)
	{
		public Sides Side { get; } = side;
		public decimal Volume { get; } = volume;
		public DateTime ActiveAfter { get; } = activeAfter;
		public decimal? StopLossPrice { get; set; }
		public decimal? TakeProfitPrice { get; set; }
	}
}