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 StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Pinball Machine: Pseudo-random entry with ATR-based risk management.
/// Uses candle hash to generate deterministic random signals.
/// </summary>
public class PinballMachineRandomDrawStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrLength;
private decimal _entryPrice;
private int _candleCount;
public PinballMachineRandomDrawStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period for stops.", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <inheritdoc />
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_candleCount = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
_candleCount = 0;
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
if (atrVal <= 0)
return;
_candleCount++;
var close = candle.ClosePrice;
// Exit management
if (Position > 0)
{
if (close >= _entryPrice + atrVal * 2m || close <= _entryPrice - atrVal * 1.5m)
{
SellMarket();
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (close <= _entryPrice - atrVal * 2m || close >= _entryPrice + atrVal * 1.5m)
{
BuyMarket();
_entryPrice = 0;
}
}
// Pseudo-random entry based on candle price hash
if (Position == 0)
{
var hash = (int)(close * 100m) ^ _candleCount;
var mod = Math.Abs(hash) % 10;
if (mod < 3)
{
_entryPrice = close;
BuyMarket();
}
else if (mod > 6)
{
_entryPrice = close;
SellMarket();
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class pinball_machine_random_draw_strategy(Strategy):
def __init__(self):
super(pinball_machine_random_draw_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
self._atr_length = self.Param("AtrLength", 14).SetDisplay("ATR Length", "ATR period for stops", "Indicators")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(pinball_machine_random_draw_strategy, self).OnReseted()
self._entry_price = 0
self._candle_count = 0
def OnStarted2(self, time):
super(pinball_machine_random_draw_strategy, self).OnStarted2(time)
self._entry_price = 0
self._candle_count = 0
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
if atr_val <= 0:
return
self._candle_count += 1
close = candle.ClosePrice
if self.Position > 0:
if close >= self._entry_price + atr_val * 2 or close <= self._entry_price - atr_val * 1.5:
self.SellMarket()
self._entry_price = 0
elif self.Position < 0:
if close <= self._entry_price - atr_val * 2 or close >= self._entry_price + atr_val * 1.5:
self.BuyMarket()
self._entry_price = 0
if self.Position == 0:
h = int(float(close) * 100) ^ self._candle_count
mod = abs(h) % 10
if mod < 3:
self._entry_price = close
self.BuyMarket()
elif mod > 6:
self._entry_price = close
self.SellMarket()
def CreateClone(self):
return pinball_machine_random_draw_strategy()