Открыть на GitHub

Стратегия VR Moving Distance

Эта стратегия StockSharp воспроизводит эксперта VR-Moving для MetaTrader 5. Она отслеживает настраиваемую скользящую среднюю и реагирует, когда цена уходит от неё на заданное количество пунктов. Алгоритм умеет наращивать позицию, умножая базовый объём ордера при последующих входах, и использует простое правило фиксации прибыли, пока открыта только одна позиция.

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

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

Индикаторы и данные

  • Одна скользящая средняя (Simple, Exponential, Smoothed, Weighted или VolumeWeighted).
  • Свечи приходят с заданным Candle Type; этот же поток используется для расчёта индикатора и торговых решений.

Правила входа

  1. На каждой завершённой свече стратегия ждёт, пока скользящая средняя полностью сформируется.
  2. Если максимум бара находится минимум на DistancePips выше средней, формируется сигнал на покупку.
  3. Если минимум бара находится минимум на DistancePips ниже средней, формируется сигнал на продажу.
  4. При смене направления текущая позиция закрывается за счёт увеличения объёма нового рыночного ордера в противоположную сторону.

Наращивание и объём

  • Первый ордер использует заданный BaseVolume.
  • Последующие ордера в ту же сторону используют BaseVolume * VolumeMultiplier.
  • Хранится максимальная цена входа для лонгов и минимальная для шортов. Новый добор возможен только после того, как цена уйдёт ещё на DistancePips от соответствующего экстремума.

Правила выхода

  • Когда открыт ровно один лонг, устанавливается цель по прибыли на уровне входной цены плюс TakeProfitPips (в ценовых единицах). Если максимум свечи достигает цели, позиция закрывается.
  • Аналогично, одиночный шорт получает цель на уровне входной цены минус TakeProfitPips и закрывается, если минимум свечи достигает отметки.
  • При наличии нескольких ордеров стратегия удерживает позицию и ждёт следующих сигналов на добор; усреднённые выходы в этом порте не реализованы.

Замечания по управлению рисками

  • При запуске вызывается StartProtection(), чтобы подключиться к стандартным защитным механизмам StockSharp.
  • Параметры расстояния и тейк-профита задаются в пунктах. Для инструментов с 3 или 5 знаками после запятой цена шага умножается на 10, как в оригинале MetaTrader.
  • Автоматический стоп-лосс не используется; риски необходимо контролировать параметрами стратегии и внешними ограничениями портфеля.

Параметры

  • Candle Type – тип данных свечи для подписки.
  • MA Length – период скользящей средней.
  • MA Type – тип сглаживания скользящей средней.
  • Price Source – цена свечи, используемая в расчёте средней.
  • Distance (pips) – минимальный разрыв в пунктах между ценой и средней для открытия позиции.
  • Take Profit (pips) – расстояние до цели по прибыли при одиночной позиции.
  • Volume Multiplier – множитель объёма для дополнительных ордеров.
  • Base Volume – базовый объём первого входа.
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;
	}
}