Ver en GitHub

Estrategia Pinball Machine

Descripción general

La Estrategia Pinball Machine es una conversión lúdica del asesor experto MetaTrader 5 "Pinball machine (barabashkakvn's edition)". En lugar de analizar la estructura del mercado, la estrategia emula una máquina de lotería: cada vela terminada activa varias extracciones aleatorias que pueden resultar en una operación si dos números coinciden. El port de StockSharp mantiene el espíritu del experto original mientras adapta la gestión de dinero y la ejecución a la API de alto nivel.

Lógica de trading

  1. Disparador – la estrategia trabaja en el marco temporal definido por Candle Type. Cuando se completa una vela, el proceso aleatorio se ejecuta una vez.
  2. Extracciones aleatorias – se generan cuatro enteros en el rango 0–100. Una configuración larga aparece si el primer par coincide y una configuración corta aparece si el segundo par coincide. Dado que las extracciones son independientes, es posible (aunque raro) generar ambas señales en la misma vela.
  3. Elegibilidad de orden – la estrategia solo coloca una nueva orden cuando no hay ninguna posición actualmente abierta. Esto mantiene la exposición neta unilateral, a diferencia del comportamiento de cobertura del original MQL.
  4. Distancias de stop/objetivo – para cada orden se producen dos números aleatorios adicionales en el rango definido por Min Offset Points y Max Offset Points. Determinan la distancia (en pasos de precio) para los niveles de stop-loss y take-profit alrededor del precio de entrada.
  5. Dimensionamiento de posición – el capital en riesgo está limitado por el parámetro Risk Percent. La estrategia estima el valor del portafolio (prefiriendo CurrentValue, luego CurrentBalance, luego BeginValue) y divide el riesgo permitido por la distancia al stop. Cuando el cálculo no es posible o resultaría en tamaño cero, el fallback es el Volume de la estrategia (predeterminado 1 lote).
  6. Ejecución de órdenes – las órdenes de mercado se emiten vía BuyMarket / SellMarket. El precio de cierre de la vela se usa como proxy para la cotización de entrada porque los datos de Bid/Ask a nivel de tick no están disponibles en el flujo de trabajo impulsado por velas.
  7. Gestión de operaciones – los niveles de stop-loss y take-profit se verifican en cada vela terminada. Si el precio penetra un nivel la posición se cierra mediante una orden de mercado, reflejando el comportamiento de las órdenes protectoras en la versión de MetaTrader.

Parámetros

  • Risk Percent – porcentaje del valor del portafolio que puede perderse si se alcanza el stop-loss. Valores superiores a cero habilitan el dimensionamiento de posición basado en riesgo.
  • Min Offset Points / Max Offset Points – límites inclusivos (expresados en pasos de precio) para seleccionar aleatoriamente las distancias de stop y objetivo. Ambos parámetros deben mantenerse positivos; la implementación los intercambia automáticamente si el mínimo supera al máximo.
  • Candle Type – la serie de datos que impulsa el motor aleatorio. Cualquier DataType compatible con SubscribeCandles puede usarse (velas de un minuto por defecto).

Diferencias con la versión MetaTrader

  • Fuente de eventos – el experto MT5 trabaja en cada tick. La estrategia StockSharp evalúa la lotería aleatoria en velas terminadas para seguir el enfoque de API de alto nivel recomendado.
  • Cobertura – MetaTrader puede acumular múltiples posiciones en ambos lados. El port se limita a una única posición neta (larga, corta o plana) porque las estrategias StockSharp típicamente son neteadas.
  • Gestión de dinero – el original dependía de CMoneyFixedMargin. La versión C# reproduce la idea usando métricas de portafolio y dimensionamiento de riesgo porcentual.
  • Colocación de órdenes – los bucles de slippage explícito y reintento son innecesarios en StockSharp y fueron eliminados. Las órdenes de mercado se envían una vez que el entorno reporta preparación (IsFormedAndOnlineAndAllowTrading).

Notas de uso

  • Asegurarse de que el instrumento seleccionado exponga un PriceStep válido. Si no está disponible, la estrategia recurre a un paso de 1 para mantener la simulación en marcha.
  • Dado que el sistema es intencionalmente aleatorio, el rendimiento variará fuertemente entre backtests. Usar la estrategia principalmente para experimentar con infraestructura, manejo de riesgo o aleatoriedad estilo Monte Carlo.
  • Ajustar el marco temporal de la vela para controlar la frecuencia con que pueden aparecer operaciones. Velas más cortas aumentan el número de loterías por sesión.
  • La estrategia dibuja tanto velas como operaciones ejecutadas en un área de gráfico cuando está disponible el charteo, lo que ayuda a diagnosticar con qué frecuencia se cumplen las condiciones aleatorias.

Notas de conversión

  • Archivo original: MQL/17744/Pinball machine.mq5.
  • Mantenidos todos los controles de entrada (porcentaje de riesgo, rangos de stop y objetivo) en forma de parámetro adecuados para optimización dentro de StockSharp.
  • La semilla aleatoria usa el predeterminado de la plataforma (Random()), que es equivalente a la llamada MathSrand(GetTickCount()) del experto 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);
	}
}