Ver en GitHub

Estrategia VR Overturn

Descripción general

  • Recrea el asesor experto MetaTrader «VR---Overturn» utilizando las APIs de alto nivel de StockSharp.
  • Mantiene solo una posición abierta a la vez y evalúa inmediatamente la siguiente operación una vez que la anterior se cierra.
  • Diseñado para traders discrecionales que desean reversión automática de posición con dimensionamiento martingala o anti-martingala.

Lógica de trading

  1. Posición inicial – la estrategia abre la primera operación en la dirección configurada (FirstPositionDirection) con el volumen base (BaseVolume).
  2. Stop loss / take profit – las órdenes de salida protectoras se adjuntan automáticamente utilizando StopLossPips y TakeProfitPips. El motor convierte pips en desplazamientos de precio absolutos analizando el paso de precio del instrumento (los instrumentos de 3 y 5 dígitos reciben el ajuste ×10 igual que en el asesor original).
  3. Procesamiento del cierre de posición – cuando una posición se cierra por cualquier orden protectora, la estrategia registra:
    • Lado de la operación cerrada (largo o corto).
    • Volumen ejecutado.
    • PnL realizado (diferencia entre precio de entrada y de salida).
  4. Dimensionamiento de la siguiente entrada – el resultado almacenado decide el lado y el tamaño del lote de la siguiente orden.
    • Las operaciones ganadoras mantienen la misma dirección, las perdedoras invierten la dirección.
    • El modo martingala multiplica el tamaño de la posición tras una pérdida y lo restablece al volumen base tras una ganancia.
    • El modo anti-martingala multiplica el tamaño de la posición tras una ganancia y lo restablece al volumen base tras una pérdida.
  5. Redondeo de lote – el tamaño calculado se ajusta al paso de volumen más cercano del instrumento antes de enviar una orden de mercado.

Parámetros

Parámetro Descripción Predeterminado
FirstPositionDirection Dirección de la primera operación (Buy/Sell). Buy
Mode Régimen de dimensionamiento: Martingale (incremento tras pérdidas) o AntiMartingale (incremento tras ganancias). Martingale
BaseVolume Volumen inicial de la posición. Se usa cuando se reinicia una secuencia. 0.1
StopLossPips Distancia al stop loss en pips. 30
TakeProfitPips Distancia al take profit en pips. 90
LotMultiplier Multiplicador aplicado durante el paso de expansión (tras pérdida para martingala, tras ganancia para anti-martingala). 1.6

Gestión de riesgos

  • Utiliza StartProtection para adjuntar órdenes de stop-loss y take-profit en cada entrada.
  • Las distancias de stop y objetivo son desplazamientos de precio absolutos derivados de los valores de pip configurados.
  • No se aplica lógica de trailing adicional, por lo que el riesgo está completamente controlado por las órdenes protectoras y las reglas de reversión de posición.

Notas operacionales

  • La estrategia no depende de velas ni indicadores; reacciona puramente a confirmaciones de operaciones (OnOwnTradeReceived).
  • Si una orden protectora se ejecuta parcialmente, la estrategia acumula el monto restante hasta que la posición esté plana antes de actuar nuevamente.
  • Los valores de comisión y swap no están disponibles en las operaciones de StockSharp, por lo que la comparación de beneficios usa solo la diferencia de precio. Considere ampliar los stops o multiplicadores si su broker cobra comisiones significativas.
  • Funciona con cualquier instrumento que proporcione metadatos de paso de precio y volumen; verifique ambos antes de implementar en producción.
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;
	}
}