Ver no GitHub

Estratégia VR Moving Distance

Esta estratégia do StockSharp replica o consultor especialista VR-Moving do MetaTrader 5. Ela monitora uma média móvel configurável e reage quando o preço se afasta além de uma distância fixa em pips. O algoritmo pode escalar em tendências multiplicando o volume base da ordem para negociações subsequentes e aplica lógica simples de take profit enquanto apenas uma posição está aberta.

Visão geral

  • Opera o instrumento atribuído à estratégia usando uma única assinatura de candles.
  • Calcula uma média móvel com comprimento, tipo de suavização e fonte de preço selecionáveis.
  • Converte as configurações de distância e take profit de pips em deslocamentos de preço usando o passo de preço do ativo.
  • Adiciona posições compradas quando o preço sobe o suficiente acima da média móvel, ou posições vendidas quando o preço cai abaixo dela.
  • Reverte a exposição líquida atual antes de abrir uma posição na direção oposta para manter a estratégia compatível com o netting do portfólio.

Indicadores e dados

  • Uma média móvel (Simple, Exponential, Smoothed, Weighted ou VolumeWeighted).
  • Os candles chegam com o Candle Type configurado; o mesmo fluxo alimenta os valores do indicador e as decisões de negociação.

Lógica de entrada

  1. A cada candle finalizado a estratégia aguarda a média móvel estar completamente formada.
  2. Se a máxima da barra estiver pelo menos DistancePips acima da média móvel, uma entrada comprada é acionada.
  3. Se a mínima da barra estiver pelo menos DistancePips abaixo da média móvel, uma entrada vendida é acionada.
  4. Ao mudar de direção a estratégia fecha a exposição existente adicionando o volume oposto à nova ordem a mercado.

Escalonamento e gestão de volume

  • A primeira ordem usa o BaseVolume configurado.
  • Ordens subsequentes na mesma direção usam BaseVolume * VolumeMultiplier.
  • O maior preço executado no lado comprado e o menor no lado vendido são rastreados. Cada nova ordem de escalonamento requer que o preço se estenda mais DistancePips a partir desse extremo antes de ser disparada.

Lógica de saída

  • Quando exatamente uma posição comprada está aberta, um alvo de lucro é colocado no preço de entrada mais TakeProfitPips (convertidos em unidades de preço). Se a máxima de um candle tocar o alvo, a posição é fechada.
  • Da mesma forma, uma única posição vendida recebe um alvo de lucro na entrada menos TakeProfitPips e fecha quando a mínima do candle o toca.
  • Uma vez que múltiplas entradas existam a estratégia mantém as posições abertas e aguarda novos sinais de escalonamento; nenhuma saída média é tentada neste port.

Notas de gestão de risco

  • StartProtection() é ativado na inicialização para se conectar aos subsistemas de proteção padrão do StockSharp.
  • Os valores de distância e take profit são medidos em pips. Para símbolos cotados com 3 ou 5 casas decimais a estratégia multiplica o passo de preço por 10 para corresponder à semântica de pips do MetaTrader.
  • Não há stop-loss automático; o risco deve ser controlado através dos parâmetros escolhidos e limites externos do portfólio.

Parâmetros

  • Candle Type – Tipo de dados usado para assinatura de candles.
  • MA Length – Período da média móvel.
  • MA Type – Método de suavização da média móvel.
  • Price Source – Preço do candle usado para calcular a média móvil.
  • Distance (pips) – Lacuna mínima em pips entre o preço e a média móvel para acionar entradas.
  • Take Profit (pips) – Distância do alvo de lucro aplicada quando apenas uma posição está aberta.
  • Volume Multiplier – Multiplicador aplicado ao volume base para entradas adicionais.
  • Base Volume – Quantidade da negociação inicial.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// VR Moving strategy converted from MetaTrader 5 expert advisor.
/// Opens positions when price deviates from a moving average by a configurable distance and scales using a multiplier.
/// </summary>
public class VrMovingDistanceStrategy : Strategy
{
	public enum MovingAverageTypes
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted,
		VolumeWeighted
	}

	public enum CandlePrices
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<MovingAverageTypes> _maType;
	private readonly StrategyParam<CandlePrices> _priceSource;
	private readonly StrategyParam<decimal> _distancePips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _baseVolume;

	private DecimalLengthIndicator _movingAverage = null!;
	private decimal _pipSize;

	private int _longEntries;
	private int _shortEntries;
	private decimal _longHighestEntry;
	private decimal _shortLowestEntry;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;

	/// <summary>
	/// Candle type used for the moving average and decision logic.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaLength
	{
		get => _maLength.Value;
		set => _maLength.Value = value;
	}

	/// <summary>
	/// Moving average smoothing type.
	/// </summary>
	public MovingAverageTypes MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Candle price source for the moving average.
	/// </summary>
	public CandlePrices PriceSource
	{
		get => _priceSource.Value;
		set => _priceSource.Value = value;
	}

	/// <summary>
	/// Distance from the moving average in pips.
	/// </summary>
	public decimal DistancePips
	{
		get => _distancePips.Value;
		set => _distancePips.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips when a single position is active.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Volume multiplier for additional entries in the same direction.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Base order volume.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public VrMovingDistanceStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "General");

		_maLength = Param(nameof(MaLength), 60)
			.SetGreaterThanZero()
			.SetDisplay("MA Length", "Moving average period", "Moving Average")
			
			.SetOptimize(10, 200, 10);

		_maType = Param(nameof(MaType), MovingAverageTypes.Exponential)
			.SetDisplay("MA Type", "Moving average smoothing method", "Moving Average");

		_priceSource = Param(nameof(PriceSource), CandlePrices.Close)
			.SetDisplay("Price Source", "Price used for the moving average", "Moving Average");

		_distancePips = Param(nameof(DistancePips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Distance (pips)", "Offset from the moving average", "Trading")
			
			.SetOptimize(10m, 150m, 10m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Exit distance when only one position is open", "Trading")
			
			.SetOptimize(10m, 150m, 10m);

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Multiplier", "Multiplier for additional entries", "Trading")
			
			.SetOptimize(1m, 3m, 0.25m);

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Volume of the initial order", "Trading");
	}

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

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

		_longEntries = 0;
		_shortEntries = 0;
		_longHighestEntry = 0m;
		_shortLowestEntry = 0m;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_pipSize = 0m;
	}

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

		UpdatePipSize();

		_movingAverage = CreateMovingAverage(MaType, MaLength, PriceSource);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_movingAverage, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _movingAverage);
			DrawOwnTrades(area);
		}

	}

	private void ProcessCandle(ICandleMessage candle, decimal maValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_movingAverage.IsFormed)
			return;


		var distance = DistancePips * _pipSize;
		var takeProfit = TakeProfitPips * _pipSize;

		var longTrigger = _longEntries == 0 ? maValue + distance : _longHighestEntry + distance;
		var shortTrigger = _shortEntries == 0 ? maValue - distance : _shortLowestEntry - distance;

		if (takeProfit > 0m)
		{
			// Close a single long position once the take profit level is reached.
			if (_longEntries == 1 && Position > 0 && _longEntryPrice.HasValue)
			{
				var target = _longEntryPrice.Value + takeProfit;
				if (candle.HighPrice >= target)
				{
					SellMarket(Position);
					ResetLongState();
				}
			}

			// Close a single short position once the take profit level is reached.
			if (_shortEntries == 1 && Position < 0 && _shortEntryPrice.HasValue)
			{
				var target = _shortEntryPrice.Value - takeProfit;
				if (candle.LowPrice <= target)
				{
					BuyMarket(Math.Abs(Position));
					ResetShortState();
				}
			}
		}

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

		// Open or scale a long position when price moves sufficiently above the moving average.
		if (candle.HighPrice >= longTrigger)
		{
			ExecuteLongEntry(longTrigger);
		}
		// Open or scale a short position when price moves sufficiently below the moving average.
		else if (candle.LowPrice <= shortTrigger)
		{
			ExecuteShortEntry(shortTrigger);
		}
	}

	private void ExecuteLongEntry(decimal triggerPrice)
	{
		var volume = _longEntries == 0 ? BaseVolume : BaseVolume * VolumeMultiplier;
		if (volume <= 0m)
			return;

		var orderVolume = volume;

		// Reverse short exposure before adding new long volume.
		if (Position < 0)
		{
			orderVolume += Math.Abs(Position);
			ResetShortState();
		}

		BuyMarket(orderVolume);

		_longEntries++;
		_longHighestEntry = _longEntries == 1 ? triggerPrice : Math.Max(_longHighestEntry, triggerPrice);
		_longEntryPrice = _longEntries == 1 ? triggerPrice : null;
	}

	private void ExecuteShortEntry(decimal triggerPrice)
	{
		var volume = _shortEntries == 0 ? BaseVolume : BaseVolume * VolumeMultiplier;
		if (volume <= 0m)
			return;

		var orderVolume = volume;

		// Reverse long exposure before adding new short volume.
		if (Position > 0)
		{
			orderVolume += Position;
			ResetLongState();
		}

		SellMarket(orderVolume);

		_shortEntries++;
		_shortLowestEntry = _shortEntries == 1 ? triggerPrice : Math.Min(_shortLowestEntry, triggerPrice);
		_shortEntryPrice = _shortEntries == 1 ? triggerPrice : null;
	}

	private void ResetLongState()
	{
		_longEntries = 0;
		_longHighestEntry = 0m;
		_longEntryPrice = null;
	}

	private void ResetShortState()
	{
		_shortEntries = 0;
		_shortLowestEntry = 0m;
		_shortEntryPrice = null;
	}

	private void UpdatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var digits = GetDecimalDigits(step);
		_pipSize = (digits == 3 || digits == 5) ? step * 10m : step;
	}

	private static int GetDecimalDigits(decimal value)
	{
		value = Math.Abs(value);
		var digits = 0;
		while (value != Math.Truncate(value) && digits < 10)
		{
			value *= 10m;
			digits++;
		}

		return digits;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type, int length, CandlePrices priceSource)
	{
		DecimalLengthIndicator indicator = type switch
		{
			MovingAverageTypes.Simple => new SimpleMovingAverage(),
			MovingAverageTypes.Exponential => new ExponentialMovingAverage(),
			MovingAverageTypes.Smoothed => new SmoothedMovingAverage(),
			MovingAverageTypes.Weighted => new WeightedMovingAverage(),
			_ => new SimpleMovingAverage(),
		};

		indicator.Length = length;
		return indicator;
	}
}