Ver en GitHub

Estrategia VR Moving Distance

Esta estrategia de StockSharp replica el asesor experto VR-Moving de MetaTrader 5. Monitorea una media móvil configurable y reacciona cuando el precio se aleja más allá de una distancia fija en pips. El algoritmo puede escalar en tendencias multiplicando el volumen base de la orden en entradas posteriores y aplica lógica simple de toma de ganancias mientras solo una posición está abierta.

Descripción general

  • Opera el instrumento asignado a la estrategia usando una única suscripción de velas.
  • Calcula una media móvil con longitud, tipo de suavizado y fuente de precio seleccionables.
  • Convierte los ajustes de distancia y toma de ganancias de pips a desplazamientos de precio usando el paso de precio del instrumento.
  • Añade posiciones largas cuando el precio sube suficientemente por encima de la media móvil, o posiciones cortas cuando el precio cae por debajo de ella.
  • Invierte la exposición neta actual antes de abrir una posición en la dirección opuesta para mantener la estrategia compatible con el neteo del portafolio.

Indicadores y datos

  • Una media móvil (Simple, Exponential, Smoothed, Weighted o VolumeWeighted).
  • Las velas llegan con el Candle Type configurado; el mismo flujo impulsa los valores del indicador y las decisiones de trading.

Lógica de entrada

  1. En cada vela finalizada la estrategia espera a que la media móvil esté completamente formada.
  2. Si el máximo de la barra está al menos DistancePips por encima de la media móvil, se activa una entrada larga.
  3. Si el mínimo de la barra está al menos DistancePips por debajo de la media móvil, se activa una entrada corta.
  4. Al cambiar de dirección la estrategia cierra la exposición existente añadiendo el volumen opuesto a la nueva orden de mercado.

Escalado y gestión de volumen

  • La primera orden usa el BaseVolume configurado.
  • Las órdenes posteriores en la misma dirección usan BaseVolume * VolumeMultiplier.
  • Se registra el precio de ejecución más alto en el lado largo y el más bajo en el lado corto. Cada nueva orden de escalado requiere que el precio se extienda otro DistancePips desde ese extremo antes de ejecutarse.

Lógica de salida

  • Cuando exactamente una posición larga está abierta, se coloca un objetivo de beneficio en el precio de entrada más TakeProfitPips (convertidos a unidades de precio). Si el máximo de una vela toca el objetivo, la posición se cierra.
  • Del mismo modo, una posición corta única recibe un objetivo de beneficio en la entrada menos TakeProfitPips y se cierra cuando el mínimo de la vela lo toca.
  • Una vez que existen múltiples entradas la estrategia mantiene las posiciones abiertas y espera nuevas señales de escalado; no se intenta una salida promediada en este puerto.

Notas de gestión de riesgo

  • StartProtection() se activa al inicio para conectarse con los subsistemas de protección estándar de StockSharp.
  • Los valores de distancia y toma de ganancias se miden en pips. Para símbolos cotizados con 3 o 5 decimales la estrategia multiplica el paso de precio por 10 para coincidir con la semántica de pips de MetaTrader.
  • No hay stop-loss automático; el riesgo debe controlarse mediante los parámetros elegidos y los límites externos del portafolio.

Parámetros

  • Candle Type – Tipo de datos usado para la suscripción de velas.
  • MA Length – Período de la media móvil.
  • MA Type – Método de suavizado de la media móvil.
  • Price Source – Precio de la vela usado para calcular la media móvil.
  • Distance (pips) – Brecha mínima en pips entre el precio y la media móvil para activar entradas.
  • Take Profit (pips) – Distancia del objetivo de beneficio aplicada cuando solo una posición está abierta.
  • Volume Multiplier – Multiplicador aplicado al volumen base para entradas adicionales.
  • Base Volume – Cantidad de la operación 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;
	}
}