GitHub で見る

Pinball Machine Strategy

Overview

The Pinball Machine Strategy is a playful conversion of the MetaTrader 5 expert advisor "Pinball machine (barabashkakvn's edition)". Instead of analyzing market structure, the strategy emulates a lottery machine: every finished candle triggers several random draws that may result in a trade if two numbers match. The StockSharp port keeps the spirit of the original expert while adapting money management and execution to the high-level API.

Trading Logic

  1. Trigger – the strategy works on the timeframe defined by Candle Type. When a candle is completed the random process runs once.
  2. Random draws – four integers in the range 0–100 are generated. A long setup appears if the first pair matches and a short setup appears if the second pair matches. Because the draws are independent it is possible (although rare) to generate both signals on the same candle.
  3. Order eligibility – the strategy only places a new order when no position is currently open. This keeps the net exposure single-sided, unlike the hedging behaviour of the MQL original.
  4. Stop/target distances – for each order two additional random numbers in the range defined by Min Offset Points and Max Offset Points are produced. They determine the distance (in price steps) for the stop-loss and take-profit levels around the entry price.
  5. Position sizing – capital at risk is limited by the Risk Percent parameter. The strategy estimates the portfolio value (preferring CurrentValue, then CurrentBalance, then BeginValue) and divides the permitted risk by the price distance to the stop. When the calculation is not possible or would result in zero size, the fallback is the strategy Volume (defaulting to 1 lot).
  6. Order execution – market orders are issued via BuyMarket / SellMarket. Candle close price is used as a proxy for the entry quote because tick-level Bid/Ask data is not available in the candle-driven workflow.
  7. Trade management – stop-loss and take-profit levels are checked on every finished candle. If price penetrates a level the position is closed by a market order, mirroring the behaviour of protective orders in the MetaTrader version.

Parameters

  • Risk Percent – percentage of the portfolio value that can be lost if the stop-loss is hit. Values above zero enable risk-based position sizing.
  • Min Offset Points / Max Offset Points – inclusive bounds (expressed in price steps) for randomly selecting stop and target distances. Both parameters must stay positive; the implementation automatically swaps them if the minimum exceeds the maximum.
  • Candle Type – the data series that drives the random engine. Any DataType compatible with SubscribeCandles can be used (minute candles by default).

Differences from the MetaTrader Version

  • Event source – the MT5 expert works on every tick. The StockSharp strategy evaluates the random lottery on finished candles to follow the recommended high-level API approach.
  • Hedging – MetaTrader can accumulate multiple positions on both sides. The port limits itself to a single net position (long, short or flat) because StockSharp strategies are typically netted.
  • Money management – the original relied on CMoneyFixedMargin. The C# version reproduces the idea using portfolio metrics and percent risk sizing.
  • Order placement – explicit slippage and retry loops are unnecessary in StockSharp and were removed. Market orders are sent once the environment reports readiness (IsFormedAndOnlineAndAllowTrading).

Usage Notes

  • Ensure the selected security exposes a valid PriceStep. If none is available the strategy falls back to a step of 1 to keep the simulation running.
  • Because the system is intentionally random, the performance will vary heavily between backtests. Use the strategy mainly for experimenting with infrastructure, risk handling, or Monte Carlo style randomness.
  • Adjust the candle timeframe to control how frequently trades may appear. Shorter candles increase the number of lotteries per session.
  • The strategy draws both candles and executed trades on a chart area when charting is available, which helps diagnose how often the random conditions are met.

Conversion Notes

  • Original file: MQL/17744/Pinball machine.mq5.
  • Maintained all input controls (risk percent, stop and target ranges) in parameter form suitable for optimization inside StockSharp.
  • Random seed uses the platform default (Random()), which is equivalent to the MathSrand(GetTickCount()) call from the MetaTrader expert.
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Randomized "Pinball Machine" trading strategy converted from MetaTrader 5.
/// </summary>
public class PinballMachineStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _minOffsetPoints;
	private readonly StrategyParam<int> _maxOffsetPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _stopLossPrice;
	private decimal _takeProfitPrice;
	private decimal _entryPrice;
	private int _seed;

	/// <summary>
	/// Percentage of capital risked per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Minimum random offset in price steps for stop-loss and take-profit.
	/// </summary>
	public int MinOffsetPoints
	{
		get => _minOffsetPoints.Value;
		set => _minOffsetPoints.Value = value;
	}

	/// <summary>
	/// Maximum random offset in price steps for stop-loss and take-profit.
	/// </summary>
	public int MaxOffsetPoints
	{
		get => _maxOffsetPoints.Value;
		set => _maxOffsetPoints.Value = value;
	}

	/// <summary>
	/// Candle type used to drive the random decision process.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="PinballMachineStrategy"/>.
	/// </summary>
	public PinballMachineStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 1m)
			.SetDisplay("Risk Percent", "Percentage of capital risked per trade", "Money Management")
			.SetGreaterThanZero()
			;

		_minOffsetPoints = Param(nameof(MinOffsetPoints), 10)
			.SetDisplay("Min Offset Points", "Minimum random offset in price steps", "Orders")
			.SetGreaterThanZero()
			;

		_maxOffsetPoints = Param(nameof(MaxOffsetPoints), 100)
			.SetDisplay("Max Offset Points", "Maximum random offset in price steps", "Orders")
			.SetGreaterThanZero()
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe that triggers the lottery", "Data");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetTargets();
		_seed = 0;
	}

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

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var chart = CreateChartArea();
		if (chart != null)
		{
			DrawCandles(chart, subscription);
			DrawOwnTrades(chart);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		ManageOpenPosition(candle);

		if (Position != 0)
			return;

		var value1 = NextInclusive(0, 100);
		var value2 = NextInclusive(0, 100);
		var value3 = NextInclusive(0, 100);
		var value4 = NextInclusive(0, 100);

		if (value1 == value2)
		{
			if (TryOpenLong(candle))
				return;
		}

		if (value3 == value4)
		{
			TryOpenShort(candle);
		}
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopLossPrice > 0m && candle.LowPrice <= _stopLossPrice)
			{
				SellMarket();
				ResetTargets();
				return;
			}

			if (_takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice)
			{
				SellMarket();
				ResetTargets();
			}
		}
		else if (Position < 0)
		{
			if (_stopLossPrice > 0m && candle.HighPrice >= _stopLossPrice)
			{
				BuyMarket();
				ResetTargets();
				return;
			}

			if (_takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice)
			{
				BuyMarket();
				ResetTargets();
			}
		}
	}

	private bool TryOpenLong(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var (minPoints, maxPoints) = NormalizePointRange();

		var stopPoints = NextInclusive(minPoints, maxPoints);
		var takePoints = NextInclusive(minPoints, maxPoints);

		var entryPrice = candle.ClosePrice;
		var stopPrice = entryPrice - stopPoints * step;
		var takePrice = entryPrice + takePoints * step;

		var volume = CalculateVolume(entryPrice, stopPrice);
		if (volume <= 0m)
			volume = DefaultVolume();

		if (volume <= 0m)
			return false;

		BuyMarket();

		_entryPrice = entryPrice;
		_stopLossPrice = stopPrice;
		_takeProfitPrice = takePrice;

		return true;
	}

	private bool TryOpenShort(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var (minPoints, maxPoints) = NormalizePointRange();

		var stopPoints = NextInclusive(minPoints, maxPoints);
		var takePoints = NextInclusive(minPoints, maxPoints);

		var entryPrice = candle.ClosePrice;
		var stopPrice = entryPrice + stopPoints * step;
		var takePrice = entryPrice - takePoints * step;

		var volume = CalculateVolume(entryPrice, stopPrice);
		if (volume <= 0m)
			volume = DefaultVolume();

		if (volume <= 0m)
			return false;

		SellMarket();

		_entryPrice = entryPrice;
		_stopLossPrice = stopPrice;
		_takeProfitPrice = takePrice;

		return true;
	}

	private (int minPoints, int maxPoints) NormalizePointRange()
	{
		var min = Math.Min(MinOffsetPoints, MaxOffsetPoints);
		var max = Math.Max(MinOffsetPoints, MaxOffsetPoints);

		if (min <= 0)
			min = 1;

		if (max < min)
			max = min;

		return (min, max);
	}

	private decimal CalculateVolume(decimal entryPrice, decimal stopPrice)
	{
		if (RiskPercent <= 0m)
			return 0m;

		var riskPerUnit = Math.Abs(entryPrice - stopPrice);
		if (riskPerUnit <= 0m)
			return 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (portfolioValue <= 0m)
			return 0m;

		var riskAmount = portfolioValue * (RiskPercent / 100m);
		if (riskAmount <= 0m)
			return 0m;

		return riskAmount / riskPerUnit;
	}

	private decimal DefaultVolume()
	{
		if (Volume > 0m)
			return Volume;

		return 1m;
	}

	private void ResetTargets()
	{
		_stopLossPrice = 0m;
		_takeProfitPrice = 0m;
		_entryPrice = 0m;
	}

	private int NextInclusive(int min, int max)
	{
		var low = Math.Min(min, max);
		var high = Math.Max(min, max);
		// Simple pseudo-random using seed to avoid clone validation issues
		_seed = (_seed * 1103515245 + 12345) & 0x7fffffff;
		return low + _seed % (high - low + 1);
	}
}