View on GitHub

Pinball Machine Random Draw Strategy

Overview

This strategy is a direct StockSharp conversion of the MetaTrader 4 expert advisor Pinball_machine.mq4. The original robot drew random integers on every incoming tick and opened a market order whenever two of those draws matched. The StockSharp version preserves the same lottery-style behaviour: on each finished candle of the selected timeframe the algorithm performs two pairs of random draws and enters a long or short market position when the corresponding pair contains equal values. Stop-loss and take-profit distances are also randomised on every evaluation, reproducing the feel of the original "pinball" routine where trades bounce in and out unpredictably.

Trading logic

  • Subscribe to candles defined by the CandleType parameter and wait for fully formed bars.
  • For every finished candle generate four integers uniformly distributed in [0, RandomMaxValue]. The first pair belongs to the potential long entry, the second pair belongs to the potential short entry.
  • Draw two additional integers between MinStopLossPoints/MaxStopLossPoints and MinTakeProfitPoints/MaxTakeProfitPoints to determine the protective distances (expressed in price steps) shared by both sides of the evaluation.
  • If the first and second random integers match, submit a market buy order with volume TradeVolume. If the third and fourth values match, submit a market sell order with the same volume. Both conditions can fire within the same candle, exactly like in the MQL version where buy and sell orders were independent events.
  • Immediately attach a stop-loss and a take-profit order (if the drawn distance is greater than zero). The distances are interpreted as multiples of the instrument’s PriceStep, mirroring the Point multiplier used in MetaTrader.

Order management and risk controls

  • StartProtection() is invoked when the strategy starts so that StockSharp manages protective orders on behalf of the strategy.
  • Each entry measures the resulting position (Position ± TradeVolume) and passes it to SetStopLoss and SetTakeProfit, which allows the platform to consolidate protective orders even when multiple trades are running at the same time.
  • If either the minimum or maximum distance parameters are set to zero or a negative number, the corresponding protection is skipped for that cycle.

Parameters

Parameter Description
TradeVolume Order size in lots/contracts submitted for every random entry.
CandleType Timeframe of the candles that trigger the random draws. Shorter periods emulate the original tick-based EA more closely.
RandomMaxValue Inclusive upper bound for the integer draws. A larger value lowers the probability of matching numbers and therefore reduces trade frequency.
MinStopLossPoints Lower bound (in price steps) for the randomly generated stop-loss distance.
MaxStopLossPoints Upper bound (in price steps) for the stop-loss distance.
MinTakeProfitPoints Lower bound (in price steps) for the randomly generated take-profit distance.
MaxTakeProfitPoints Upper bound (in price steps) for the take-profit distance.
RandomSeed Seed of the pseudo-random number generator. Zero keeps the behaviour time-based, any other value makes the sequence reproducible.

Implementation notes

  • The MetaTrader script was tick-driven; the StockSharp port uses candle completions because the high-level API operates on time-series events. Setting a very short CandleType (for example one-second or tick candles) restores the fast-paced nature of the original.
  • Stop-loss and take-profit values are generated once per evaluation and reused for both the long and short branches, exactly like in the source EA.
  • Ensure that the traded instrument exposes a valid PriceStep, otherwise protective distances expressed in points may need manual adjustment.
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; }
	}
}