Ver no GitHub

Estratégia VR Overturn

Visão geral

  • Recria o expert MetaTrader «VR---Overturn» utilizando as APIs de alto nível do StockSharp.
  • Mantém apenas uma posição aberta por vez e avalia imediatamente a próxima operação assim que a anterior é fechada.
  • Desenvolvido para traders discricionários que desejam reversão automática de posição com dimensionamento martingale ou anti-martingale.

Lógica de negociação

  1. Posição inicial – a estratégia abre a primeira operação na direção configurada (FirstPositionDirection) com o volume base (BaseVolume).
  2. Stop loss / take profit – ordens de saída protetoras são anexadas automaticamente usando StopLossPips e TakeProfitPips. O motor converte pips em deslocamentos de preço absolutos analisando o passo de preço do instrumento (instrumentos de 3 e 5 dígitos recebem o ajuste ×10, assim como no expert original).
  3. Processamento do fechamento de posição – quando uma posição é fechada por qualquer ordem protetora, a estratégia registra:
    • Lado da operação fechada (comprado ou vendido).
    • Volume executado.
    • PnL realizado (diferença entre preço de entrada e de saída).
  4. Dimensionamento da próxima entrada – o resultado armazenado decide o lado e o tamanho do lote da próxima ordem.
    • Operações vencedoras mantêm a mesma direção, operações perdedoras invertem a direção.
    • O modo martingale multiplica o tamanho da posição após uma perda e o redefine para o volume base após um ganho.
    • O modo anti-martingale multiplica o tamanho da posição após um ganho e o redefine para o volume base após uma perda.
  5. Arredondamento de lote – o tamanho calculado é ajustado para o passo de volume mais próximo do instrumento antes de enviar uma ordem a mercado.

Parâmetros

Parâmetro Descrição Padrão
FirstPositionDirection Direção da primeira operação (Buy/Sell). Buy
Mode Regime de dimensionamento: Martingale (aumento após perdas) ou AntiMartingale (aumento após ganhos). Martingale
BaseVolume Volume inicial da posição. Usado quando uma sequência é reiniciada. 0.1
StopLossPips Distância ao stop loss em pips. 30
TakeProfitPips Distância ao take profit em pips. 90
LotMultiplier Multiplicador aplicado durante o passo de expansão (após perda para martingale, após ganho para anti-martingale). 1.6

Gestão de riscos

  • Utiliza StartProtection para anexar ordens de stop-loss e take-profit em cada entrada.
  • As distâncias de stop e alvo são deslocamentos de preço absolutos derivados dos valores de pip configurados.
  • Nenhuma lógica de trailing adicional é aplicada, portanto o risco é inteiramente controlado pelas ordens protetoras e pelas regras de reversão de posição.

Notas operacionais

  • A estratégia não depende de velas nem indicadores; reage puramente a confirmações de operações (OnOwnTradeReceived).
  • Se uma ordem protetora for parcialmente executada, a estratégia acumula o valor restante até que a posição esteja zerada antes de agir novamente.
  • Valores de comissão e swap não estão disponíveis nas operações do StockSharp, portanto a comparação de lucro usa apenas a diferença de preço. Considere ampliar stops ou multiplicadores se sua corretora cobrar taxas significativas.
  • Funciona com qualquer instrumento que forneça metadados de passo de preço e volume; verifique ambos antes de implantar em produção.
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;
	}
}