Стратегия представляет собой прямую конвертацию советника MetaTrader 4 Pinball_machine.mq4 в среду StockSharp. В исходном
советнике на каждом тике генерировались случайные числа и при совпадении пары значений открывалась рыночная позиция. В версии
для StockSharp логика сохранена: на каждой закрывшейся свече выбранного таймфрейма выполняются две серии случайных бросков, и при
совпадении чисел стратегия открывает длинную или короткую позицию. Дистанции стоп-лосса и тейк-профита также выбираются случайным
образом при каждом проходе, что сохраняет “пинбольный” характер торговли.
Логика работы
Подписаться на свечи типа CandleType и ждать их завершения.
Для каждой закрывшейся свечи сгенерировать четыре равномерно распределённых целых числа в диапазоне [0, RandomMaxValue]. Первая
пара относится к потенциальной покупке, вторая — к потенциальной продаже.
Дополнительно получить два случайных значения в пределах MinStopLossPoints/MaxStopLossPoints и MinTakeProfitPoints/
MaxTakeProfitPoints, определяющие расстояние до защитных ордеров (в шагах цены). Эти расстояния используются обеими сторонами
одновременно.
Если первое и второе значения совпали — отправить рыночную заявку на покупку объёмом TradeVolume. Если совпали третье и четвёртое
значения — отправить рыночную заявку на продажу тем же объёмом. Оба условия могут выполниться в рамках одной свечи, как и в MQL-версии.
При наличии положительных защитных расстояний немедленно повесить тейк-профит и стоп-лосс. Значения интерпретируются как множители
PriceStep, аналогично тому, как оригинал умножал на Point.
Управление ордерами и рисками
При запуске вызывается StartProtection(), чтобы StockSharp автоматически сопровождал защитные заявки.
После открытия позиции вычисляется результирующий размер позиции (Position ± TradeVolume), и именно он передаётся в методы
SetStopLoss и SetTakeProfit, что позволяет объединять защитные ордера даже при одновременных сделках.
Если минимальные или максимальные значения дистанций равны нулю либо отрицательны, соответствующие защитные ордера в текущем цикле
не выставляются.
Параметры
Параметр
Описание
TradeVolume
Объём рыночного ордера для каждого случайного входа.
CandleType
Тип свечей, по закрытию которых выполняются случайные броски. Короткие интервалы имитируют тиковый режим исходного советника.
RandomMaxValue
Верхняя граница (включительно) для случайных чисел. Чем больше значение, тем реже совпадения и тем ниже частота сделок.
MinStopLossPoints
Нижняя граница дистанции стоп-лосса (в шагах цены).
MaxStopLossPoints
Верхняя граница дистанции стоп-лосса.
MinTakeProfitPoints
Нижняя граница дистанции тейк-профита.
MaxTakeProfitPoints
Верхняя граница дистанции тейк-профита.
RandomSeed
Зерно генератора псевдослучайных чисел. Ноль — инициализация по текущему времени, любое другое значение делает последовательность повторяемой.
Особенности реализации
Исходный советник реагировал на каждый тик, поэтому в StockSharp используется закрытие свечей — так работает высокоуровневый API. Выбор очень короткого таймфрейма (секундные или тиковые свечи) позволяет приблизиться к оригинальному темпу.
Значения стоп-лосса и тейк-профита генерируются один раз на проход и применяются одновременно для покупок и продаж — как в MQL-версии.
Убедитесь, что для выбранного инструмента задан корректный PriceStep, иначе дистанции, выраженные в пунктах, потребуют ручной корректировки.
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()