Открыть на GitHub

Стратегия Pinball Machine

Общее описание

Pinball Machine — это перенос экспертной системы MetaTrader 5 «Pinball machine (barabashkakvn's edition)» на платформу StockSharp. Вместо анализа рынка стратегия имитирует игровой автомат: каждое закрытие свечи запускает серию случайных розыгрышей, и если значения совпадают, открывается позиция. Переписанная версия сохраняет дух оригинала, но использует высокоуровневый API StockSharp для управления капиталом и исполнением сделок.

Логика работы

  1. Триггер – стратегия обрабатывает данные в формате, заданном параметром Candle Type. После завершения свечи запускается один цикл генерации случайных чисел.
  2. Случайные розыгрыши – формируется четыре числа от 0 до 100. Совпадение первой пары даёт сигнал на покупку, совпадение второй – на продажу. Пары независимы, поэтому теоретически можно получить оба сигнала в пределах одной свечи.
  3. Условия открытия – новая сделка отправляется только при отсутствии открытой позиции. Это ограничивает стратегию одним нетто-положением и отличает от хеджирующего поведения оригинального советника.
  4. Дистанции стопов и целей – для выбранного направления генерируются ещё два числа в диапазоне Min Offset PointsMax Offset Points. Они переводятся в величину шага цены и задают смещения для стоп-лосса и тейк-профита относительно цены входа.
  5. Размер позиции – параметр Risk Percent ограничивает величину капитала под риском. Стратегия оценивает стоимость портфеля (в порядке приоритета CurrentValue, CurrentBalance, затем BeginValue) и делит допустимый риск на расстояние до стопа. Если расчёт невозможен, используется значение Volume (по умолчанию 1 контракт/лот).
  6. Исполнение – сделки отправляются рыночными ордерами BuyMarket и SellMarket. Цена закрытия свечи выступает прокси для входа, так как в свечном подписке нет непосредственных котировок Bid/Ask.
  7. Сопровождение позиции – на каждой завершённой свече проверяется достижение стоп-лосса или тейк-профита. При пробое уровня позиция закрывается рыночным ордером, что повторяет поведение защитных ордеров в версии MT5.

Параметры

  • Risk Percent – доля стоимости портфеля, которой стратегия готова рискнуть в одной сделке. Значения больше нуля включают расчёт позиции от риска.
  • Min Offset Points / Max Offset Points – минимальное и максимальное (включительно) смещение в шагах цены для случайного выбора стопа и цели. Оба параметра должны быть положительными; при перепутанном порядке значения меняются местами автоматически.
  • Candle Type – тип данных, который инициирует «лотерею». По умолчанию используется минутная свеча, но допустим любой DataType, поддерживаемый SubscribeCandles.

Отличия от версии MetaTrader

  • Источник событий – оригинальный советник работает по каждому тиковому обновлению. Вариант на StockSharp выполняет логику по закрытию свечи согласно рекомендациям по высокоуровневому API.
  • Хеджирование – в MT5 возможно накапливать разнонаправленные позиции. Перенос ограничен одним нетто-положением (лонг, шорт или вне рынка), что типично для стратегий StockSharp.
  • Управление капиталом – вместо компонента CMoneyFixedMargin используется вычисление объёма через стоимость портфеля и процент риска.
  • Исполнение заявок – убраны циклы повторных запросов котировок и установка проскальзывания. Ордера отправляются сразу после проверки IsFormedAndOnlineAndAllowTrading.

Рекомендации по использованию

  • Убедитесь, что инструмент имеет корректный PriceStep. При отсутствии шага стратегия применит значение 1, чтобы продолжить моделирование.
  • Из-за случайной природы результат тестирования будет сильно варьироваться. Стратегия подходит для изучения инфраструктуры, риск-менеджмента и экспериментов в стиле Монте-Карло.
  • Меняя период свечи, можно управлять частотой розыгрышей: короткие интервалы дают больше потенциальных сделок.
  • При наличии графической панели стратегия выводит свечи и собственные сделки, что облегчает оценку частоты совпадений.

Примечания по конверсии

  • Исходный файл: MQL/17744/Pinball machine.mq5.
  • Все входные настройки (процент риска, диапазоны стопа и цели) вынесены в параметры StockSharp и пригодны для оптимизации.
  • Генератор случайных чисел использует стандартный Random() .NET, что соответствует инициализации MathSrand(GetTickCount()) в MetaTrader.
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);
	}
}