Auf GitHub ansehen

VR Overturn Strategie

Überblick

  • Repliziert den MetaTrader-Experten «VR---Overturn» mit den High-Level-APIs von StockSharp.
  • Hält jeweils nur eine offene Position und bewertet den nächsten Trade sofort nach dem Schließen des vorherigen.
  • Entwickelt für diskretionäre Trader, die eine automatische Positionsumkehr mit Martingale- oder Anti-Martingale-Sizing wünschen.

Handelslogik

  1. Einstiegsposition – die Strategie eröffnet den ersten Trade in der konfigurierten Richtung (FirstPositionDirection) mit dem Basisvolumen (BaseVolume).
  2. Stop-Loss / Take-Profit – Schutzausstiegsorders werden automatisch mit StopLossPips und TakeProfitPips verknüpft. Die Engine konvertiert Pips in absolute Preisoffsets durch Analyse des Kursschritts des Instruments (Instrumente mit 3 und 5 Dezimalstellen erhalten die 10-fache Anpassung wie beim Original-Experten).
  3. Verarbeitung des Positionsschlusses – wenn eine Position durch eine Schutzorder geschlossen wird, erfasst die Strategie:
    • Seite des geschlossenen Trades (Long oder Short).
    • Ausgeführtes Volumen.
    • Realisierter PnL (Differenz zwischen Ein- und Ausstiegspreis).
  4. Sizing der nächsten Einstiegsposition – das gespeicherte Ergebnis bestimmt die Seite und die Lotgröße der nächsten Order.
    • Gewinnende Trades behalten dieselbe Richtung bei, verlierende Trades drehen die Richtung um.
    • Der Martingale-Modus multipliziert die Positionsgröße nach einem Verlust und setzt sie nach einem Gewinn auf das Basisvolumen zurück.
    • Der Anti-Martingale-Modus multipliziert die Positionsgröße nach einem Gewinn und setzt sie nach einem Verlust auf das Basisvolumen zurück.
  5. Lot-Rundung – die berechnete Größe wird auf den nächsten Volumenschritt des Instruments abgeschnitten, bevor eine Marktorder gesendet wird.

Parameter

Parameter Beschreibung Standard
FirstPositionDirection Richtung des allerersten Trades (Buy/Sell). Buy
Mode Sizing-Modus: Martingale (Erhöhung nach Verlusten) oder AntiMartingale (Erhöhung nach Gewinnen). Martingale
BaseVolume Initiales Positionsvolumen. Wird beim Zurücksetzen einer Sequenz verwendet. 0.1
StopLossPips Abstand zum Stop-Loss in Pips. 30
TakeProfitPips Abstand zum Take-Profit in Pips. 90
LotMultiplier Multiplikator, der beim Erweiterungsschritt angewendet wird (nach Verlust für Martingale, nach Gewinn für Anti-Martingale). 1.6

Risikomanagement

  • Verwendet StartProtection, um Stop-Loss- und Take-Profit-Orders für jeden Einstieg zu verknüpfen.
  • Stop- und Zielabstände sind absolute Preisoffsets, abgeleitet aus den konfigurierten Pip-Werten.
  • Es wird keine zusätzliche Trailing-Logik angewendet, sodass das Risiko vollständig durch die Schutzorders und die Positionsumkehrregeln kontrolliert wird.

Betriebshinweise

  • Die Strategie basiert nicht auf Kerzen oder Indikatoren; sie reagiert ausschließlich auf Trade-Bestätigungen (OnOwnTradeReceived).
  • Wenn eine Schutzorder teilweise ausgeführt wird, akkumuliert die Strategie den verbleibenden Betrag, bis die Position flat ist, bevor sie erneut handelt.
  • Provisions- und Swap-Werte sind in StockSharp-Trades nicht verfügbar, daher verwendet der Gewinnvergleich nur die Preisdifferenz. Erwägen Sie, Stops oder Multiplikatoren zu erweitern, wenn Ihr Broker erhebliche Gebühren berechnet.
  • Funktioniert mit jedem Instrument, das Preis- und Volumenschrittmetadaten bereitstellt; überprüfen Sie beide vor dem Einsatz in der Produktion.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// VR Overturn strategy that alternates between martingale and anti-martingale sizing rules.
/// It opens a single position at a time and reverses direction after losses while
/// optionally increasing size after wins depending on the selected mode.
/// </summary>
public class VrOverturnStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _volumeEpsilon;

	public enum InitialDirections
	{
		/// <summary>
		/// Start with a long position.
		/// </summary>
		Buy,

		/// <summary>
		/// Start with a short position.
		/// </summary>
		Sell
	}

	public enum TradeModes
	{
		/// <summary>
		/// Increase size after losses and reset after wins.
		/// </summary>
		Martingale,

		/// <summary>
		/// Increase size after wins and reset after losses.
		/// </summary>
		AntiMartingale
	}

	private readonly StrategyParam<InitialDirections> _initialDirection;
	private readonly StrategyParam<TradeModes> _tradeMode;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<decimal> _lotMultiplier;

	private decimal _pipSize;
	private Sides? _pendingEntrySide;
	private Sides? _activeSide;
	private decimal _entryPrice;
	private decimal _openedVolume;
	private decimal _closedVolume;
	private decimal _realizedPnL;

	private decimal _lastClosedVolume;
	private decimal _lastClosedProfit;
	private Sides? _lastClosedSide;
	private bool _hasClosedHistory;

	/// <summary>
	/// Initializes a new instance of the <see cref="VrOverturnStrategy"/> class.
	/// </summary>
	public VrOverturnStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type for data feed", "General");

		_volumeEpsilon = Param(nameof(VolumeEpsilon), 1e-6m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Epsilon", "Minimum volume threshold to treat position as flat", "Risk");

		_initialDirection = Param(nameof(FirstPositionDirection), InitialDirections.Buy)
			.SetDisplay("Initial Direction", "Direction of the very first trade", "Trading");

		_tradeMode = Param(nameof(Mode), TradeModes.Martingale)
			.SetDisplay("Trading Mode", "Choose martingale or anti-martingale sizing", "Trading");

		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Initial order size", "Risk")
			;

		_stopLossPips = Param(nameof(StopLossPips), 300)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Distance to stop loss in pips", "Risk")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 900)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance to take profit in pips", "Risk")
			;

		_lotMultiplier = Param(nameof(LotMultiplier), 1.6m)
			.SetGreaterThanZero()
			.SetDisplay("Lot Multiplier", "Multiplier applied after losses or wins", "Risk")
			;
	}

	/// <summary>
	/// Candle type for data feed.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Volume tolerance used to consider a position fully closed.
	/// </summary>
	public decimal VolumeEpsilon
	{
		get => _volumeEpsilon.Value;
		set => _volumeEpsilon.Value = value;
	}

	/// <summary>
	/// Direction of the very first position.
	/// </summary>
	public InitialDirections FirstPositionDirection
	{
		get => _initialDirection.Value;
		set => _initialDirection.Value = value;
	}

	/// <summary>
	/// Selected sizing regime.
	/// </summary>
	public TradeModes Mode
	{
		get => _tradeMode.Value;
		set => _tradeMode.Value = value;
	}

	/// <summary>
	/// Base contract volume used for new sequences.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Multiplier applied after wins or losses depending on the selected mode.
	/// </summary>
	public decimal LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_pendingEntrySide = null;
		_activeSide = null;
		_entryPrice = 0m;
		_openedVolume = 0m;
		_closedVolume = 0m;
		_realizedPnL = 0m;

		_lastClosedVolume = 0m;
		_lastClosedProfit = 0m;
		_lastClosedSide = null;
		_hasClosedHistory = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = CalculatePipSize();

		var stopDistance = StopLossPips * _pipSize;
		var takeDistance = TakeProfitPips * _pipSize;

		// Subscribe to candles so the backtest emulator has price data.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(OnCandle).Start();

		StartProtection(
		takeProfit: new Unit(takeDistance, UnitTypes.Absolute),
		stopLoss: new Unit(stopDistance, UnitTypes.Absolute),
		useMarketOrders: true);
	}

	private void OnCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Try to open a position if flat (handles both initial and post-exit entries).
		if (Position == 0 && !_pendingEntrySide.HasValue)
			TryOpenNextPosition();
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		if (trade?.Order == null)
		return;

		var side = trade.Order.Side;
		var volume = trade.Trade.Volume;
		var price = trade.Trade.Price;

		if (volume <= 0m)
		return;

		if (_pendingEntrySide.HasValue && side == _pendingEntrySide.Value)
		{
			RegisterEntry(price, volume, side);
			_pendingEntrySide = null;
			return;
		}

		if (_activeSide == null)
		return;

		if (side == _activeSide)
		{
			RegisterEntry(price, volume, side);
			return;
		}

		RegisterExit(price, volume);
	}

	private void RegisterEntry(decimal price, decimal volume, Sides side)
	{
		var previousVolume = _openedVolume;
		_openedVolume += volume;

		_entryPrice = previousVolume <= 0m
		? price
		: (_entryPrice * previousVolume + price * volume) / _openedVolume;

		_activeSide = side;
	}

	private void RegisterExit(decimal price, decimal volume)
	{
		_closedVolume += volume;

		var profit = _activeSide == Sides.Buy
		? (price - _entryPrice) * volume
		: (_entryPrice - price) * volume;

		_realizedPnL += profit;

		if (_closedVolume + VolumeEpsilon < _openedVolume)
		return;

		var closedVolume = _openedVolume;

		_lastClosedSide = _activeSide;
		_lastClosedVolume = closedVolume;
		_lastClosedProfit = _realizedPnL;
		_hasClosedHistory = true;

		_activeSide = null;
		_openedVolume = 0m;
		_closedVolume = 0m;
		_realizedPnL = 0m;
		_entryPrice = 0m;

		TryOpenNextPosition();
	}

	private void TryOpenNextPosition()
	{
		// Strategy has no indicators to check formation on.

		if (Position != 0 || _pendingEntrySide.HasValue)
		return;

		var baseVolume = AdjustVolume(BaseVolume);
		if (baseVolume <= 0m)
		return;

		Sides nextSide;
		decimal orderVolume;

		if (!_hasClosedHistory)
		{
			nextSide = FirstPositionDirection == InitialDirections.Buy ? Sides.Buy : Sides.Sell;
			orderVolume = baseVolume;
		}
		else if (_lastClosedSide.HasValue)
		{
			var referenceVolume = _lastClosedVolume > 0m ? _lastClosedVolume : baseVolume;

			if (_lastClosedProfit > 0m && Mode == TradeModes.Martingale)
			referenceVolume = baseVolume;

			if (_lastClosedProfit < 0m && Mode == TradeModes.AntiMartingale)
			referenceVolume = baseVolume;

			if (_lastClosedSide == Sides.Buy)
			{
				if (_lastClosedProfit > 0m)
				{
					nextSide = Sides.Buy;
					orderVolume = referenceVolume * GetWinningMultiplier();
				}
				else if (_lastClosedProfit < 0m)
				{
					nextSide = Sides.Sell;
					orderVolume = referenceVolume * GetLosingMultiplier();
				}
				else
				{
					return;
				}
			}
			else
			{
				if (_lastClosedProfit > 0m)
				{
					nextSide = Sides.Sell;
					orderVolume = referenceVolume * GetWinningMultiplier();
				}
				else if (_lastClosedProfit < 0m)
				{
					nextSide = Sides.Buy;
					orderVolume = referenceVolume * GetLosingMultiplier();
				}
				else
				{
					return;
				}
			}
		}
		else
		{
			nextSide = FirstPositionDirection == InitialDirections.Buy ? Sides.Buy : Sides.Sell;
			orderVolume = baseVolume;
		}

		orderVolume = AdjustVolume(orderVolume);
		if (orderVolume <= 0m)
		return;

		_pendingEntrySide = nextSide;

		if (nextSide == Sides.Buy)
		BuyMarket();
		else
		SellMarket();
	}

	private decimal GetWinningMultiplier()
	{
		return Mode == TradeModes.Martingale ? 1m : LotMultiplier;
	}

	private decimal GetLosingMultiplier()
	{
		return Mode == TradeModes.Martingale ? LotMultiplier : 1m;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;

		if (step <= 0m)
		return 1m;

		var temp = step;
		var digits = 0;

		while (temp < 1m && digits < 10)
		{
			temp *= 10m;
			digits++;
		}

		return digits == 3 || digits == 5 ? step * 10m : step;
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (volume <= 0m)
		return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var stepsCount = Math.Floor(volume / step);
			volume = stepsCount * step;
		}

		return volume;
	}
}