Открыть на GitHub

Стратегия VR Overturn

Общее описание

  • Переносит советник MetaTrader «VR---Overturn» на высокоуровневый API StockSharp.
  • В любой момент времени удерживает только одну позицию и сразу рассчитывает следующий вход после закрытия предыдущего.
  • Подходит трейдерам, которым нужна автоматическая смена направления позиции с режимами мартингейла и анти-мартингейла.

Логика торговли

  1. Первичный вход — первая сделка открывается в направлении, заданном параметром FirstPositionDirection, базовым объёмом BaseVolume.
  2. Стоп и тейк — защитные ордера выставляются автоматически на основе параметров StopLossPips и TakeProfitPips. Размер пункта пересчитывается в абсолютное смещение по цене с учётом шага цены инструмента (для 3- и 5-значных котировок применяется умножение на 10, как в оригинальном советнике).
  3. Обработка закрытия — при закрытии позиции совет фиксирует:
    • направление закрытой сделки;
    • фактический объём;
    • реализованную прибыль/убыток (разницу между ценой входа и выхода).
  4. Расчёт следующего входа — сохранённый результат определяет направление и объём следующего ордера.
    • Выигрышные сделки продолжают движение в ту же сторону, убыточные — разворачиваются.
    • В режиме мартингейла объём увеличивается после убытка и возвращается к базовому после прибыли.
    • В режиме анти-мартингейла объём увеличивается после прибыли и возвращается к базовому после убытка.
  5. Округление лота — рассчитанный объём подгоняется под ближайший шаг объёма инструмента перед отправкой рыночного ордера.

Параметры

Параметр Описание Значение по умолчанию
FirstPositionDirection Направление первой сделки (Buy/Sell). Buy
Mode Режим управления объёмом: Martingale (рост после убытков) или AntiMartingale (рост после прибыли). Martingale
BaseVolume Базовый объём позиции, используемый при сбросе серии. 0.1
StopLossPips Расстояние до стоп-лосса в пунктах. 30
TakeProfitPips Расстояние до тейк-профита в пунктах. 90
LotMultiplier Множитель, применяемый на шаге увеличения объёма (после убытка для мартингейла, после прибыли для анти-мартингейла). 1.6

Управление рисками

  • Используется StartProtection для привязки стоп-лосса и тейк-профита к каждому входу.
  • Расстояния стопа и цели пересчитываются в абсолютное смещение цены исходя из заданного количества пунктов.
  • Дополнительного трейлинга нет, поэтому риск контролируется защитными ордерами и правилами разворота.

Эксплуатационные заметки

  • Стратегия не использует свечи и индикаторы, вся логика строится на обработке торговых подтверждений (OnOwnTradeReceived).
  • При частичном исполнении защитного ордера объём суммируется до полного закрытия позиции, после чего разрешается следующий вход.
  • Комиссии и свопы в объектах StockSharp недоступны, поэтому сравнение прибыли выполняется только по разнице цен. При высоких комиссиях стоит увеличить стопы или множитель.
  • Требуется инструмент с определённым шагом цены и объёма — проверьте эти свойства перед запуском на реальном счёте.
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;
	}
}