Auf GitHub ansehen

Pinball Machine-Strategie

Übersicht

Die Pinball Machine-Strategie ist eine spielerische Konvertierung des MetaTrader-5-Expertenberaters "Pinball machine (barabashkakvn's edition)". Anstatt die Marktstruktur zu analysieren, emuliert die Strategie eine Lottermaschine: Jede fertiggestellte Kerze löst mehrere Zufallsziehungen aus, die zu einem Trade führen können, wenn zwei Zahlen übereinstimmen. Der StockSharp-Port bewahrt den Geist des ursprünglichen Experten und passt gleichzeitig Geldverwaltung und Ausführung an die High-Level-API an.

Handelslogik

  1. Auslöser – die Strategie arbeitet auf dem durch Candle Type definierten Zeitrahmen. Wenn eine Kerze abgeschlossen ist, läuft der Zufallsprozess einmal ab.
  2. Zufallsziehungen – vier ganze Zahlen im Bereich 0–100 werden generiert. Ein Long-Setup erscheint, wenn das erste Paar übereinstimmt, und ein Short-Setup erscheint, wenn das zweite Paar übereinstimmt. Da die Ziehungen unabhängig sind, ist es möglich (wenn auch selten), beide Signale auf derselben Kerze zu generieren.
  3. Ordereignung – die Strategie platziert nur dann eine neue Order, wenn derzeit keine Position offen ist. Dies hält die Nettoexposition einseitig, anders als das Hedging-Verhalten des MQL-Originals.
  4. Stop/Ziel-Distanzen – für jede Order werden zwei zusätzliche Zufallszahlen im durch Min Offset Points und Max Offset Points definierten Bereich erzeugt. Sie bestimmen den Abstand (in Preisschritten) für die Stop-Loss- und Take-Profit-Level rund um den Einstandspreis.
  5. Positionsgrößenbestimmung – das riskierte Kapital ist durch den Risk Percent-Parameter begrenzt. Die Strategie schätzt den Portfolio-Wert (bevorzugt CurrentValue, dann CurrentBalance, dann BeginValue) und teilt das erlaubte Risiko durch den Preisabstand zum Stop. Wenn die Berechnung nicht möglich ist oder null ergeben würde, ist der Fallback das Strategie-Volume (Standard: 1 Lot).
  6. Orderausführung – Marktorders werden über BuyMarket / SellMarket ausgegeben. Kerzenschlusskurs wird als Proxy für die Einstandskotierung verwendet, da Tick-Level-Bid/Ask-Daten im kerzengetriebenen Workflow nicht verfügbar sind.
  7. Handelsverwaltung – Stop-Loss- und Take-Profit-Level werden bei jeder fertiggestellten Kerze überprüft. Wenn der Preis ein Level durchdringt, wird die Position durch eine Marktorder geschlossen, was das Verhalten von Schutzorders in der MetaTrader-Version widerspiegelt.

Parameter

  • Risk Percent – Prozentsatz des Portfolio-Werts, der verloren gehen kann, wenn der Stop-Loss ausgelöst wird. Werte über null aktivieren risikobasierte Positionsgrößenbestimmung.
  • Min Offset Points / Max Offset Points – inklusive Grenzen (ausgedrückt in Preisschritten) für die zufällige Auswahl von Stop- und Zieldistanzen. Beide Parameter müssen positiv bleiben; die Implementierung tauscht sie automatisch aus, wenn das Minimum das Maximum übersteigt.
  • Candle Type – die Datenserie, die den Zufallsmotor antreibt. Jeder DataType, der mit SubscribeCandles kompatibel ist, kann verwendet werden (standardmäßig Minutenkerzen).

Unterschiede zur MetaTrader-Version

  • Ereignisquelle – der MT5-Experte arbeitet bei jedem Tick. Die StockSharp-Strategie wertet die Zufallslotterie bei fertigen Kerzen aus, um dem empfohlenen High-Level-API-Ansatz zu folgen.
  • Hedging – MetaTrader kann mehrere Positionen auf beiden Seiten ansammeln. Der Port beschränkt sich auf eine einzige Nettoposition (long, short oder flat), da StockSharp-Strategien typischerweise netto abgewickelt werden.
  • Geldverwaltung – das Original stützte sich auf CMoneyFixedMargin. Die C#-Version reproduziert die Idee mit Portfolio-Metriken und prozentualer Risikodimensionierung.
  • Orderplatzierung – explizite Slippage- und Wiederholungsschleifen sind in StockSharp unnötig und wurden entfernt. Marktorders werden einmal gesendet, nachdem die Umgebung Bereitschaft meldet (IsFormedAndOnlineAndAllowTrading).

Verwendungshinweise

  • Sicherstellen, dass das ausgewählte Instrument einen gültigen PriceStep enthüllt. Falls keiner verfügbar ist, fällt die Strategie auf einen Schritt von 1 zurück, um die Simulation am Laufen zu halten.
  • Da das System intentionell zufällig ist, wird die Performance stark zwischen Backtests variieren. Die Strategie hauptsächlich für Experimente mit Infrastruktur, Risikobehandlung oder Monte-Carlo-artiger Zufälligkeit verwenden.
  • Den Kerzen-Zeitrahmen anpassen, um zu steuern, wie häufig Trades erscheinen können. Kürzere Kerzen erhöhen die Anzahl der Lotterien pro Sitzung.
  • Die Strategie zeichnet sowohl Kerzen als auch ausgeführte Trades auf einem Chartbereich, wenn Charting verfügbar ist, was hilft zu diagnostizieren, wie oft die Zufallsbedingungen erfüllt werden.

Konvertierungshinweise

  • Originaldatei: MQL/17744/Pinball machine.mq5.
  • Alle Eingabesteuerungen (Risikoprozent, Stop- und Zielbereiche) in Parameterform gehalten, geeignet für Optimierung innerhalb von StockSharp.
  • Zufallssamen verwendet den Plattform-Standard (Random()), was dem MathSrand(GetTickCount())-Aufruf des MetaTrader-Experten entspricht.
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);
	}
}