GitHub で見る

VR Moving Distance Strategy

This StockSharp strategy replicates the VR-Moving expert advisor from MetaTrader 5. It watches a configurable moving average and reacts when price drifts beyond a fixed pip distance. The algorithm can scale into trends by multiplying the base order volume for follow-up trades and applies simple take-profit logic while only one position is open.

Overview

  • Trades the instrument assigned to the strategy using a single candle subscription.
  • Computes a moving average with selectable length, smoothing type, and price source.
  • Converts distance and take-profit settings from pips into price offsets using the security price step.
  • Adds long positions when price rises far enough above the moving average, or short positions when price falls below it.
  • Reverses the current net exposure before opening a position in the opposite direction to keep the portfolio netting-friendly.

Indicators and Data

  • One moving average (Simple, Exponential, Smoothed, Weighted, or VolumeWeighted).
  • Candles arrive with the configured Candle Type; the same stream drives indicator values and trading decisions.

Entry Logic

  1. On each finished candle the strategy waits for the moving average to be fully formed.
  2. If the high of the bar is at least DistancePips above the moving average, a long entry is triggered.
  3. If the low of the bar is at least DistancePips below the moving average, a short entry is triggered.
  4. When switching direction the strategy closes the existing exposure by adding the opposite volume to the new market order.

Scaling and Volume Management

  • The first order uses the configured BaseVolume.
  • Subsequent orders in the same direction use BaseVolume * VolumeMultiplier.
  • The highest filled price on the long side and the lowest filled price on the short side are tracked. Each new scaling order requires price to extend by another DistancePips from that extreme before firing.

Exit Logic

  • When exactly one long position is open, a profit target is placed at the entry price plus TakeProfitPips (converted to price units). If a candle high touches the target, the position is closed.
  • Similarly, a single short position receives a profit target at entry minus TakeProfitPips and closes when the candle low touches it.
  • Once multiple entries exist the strategy keeps the positions open and waits for new scaling signals; no averaged exit is attempted in this port.

Risk Management Notes

  • StartProtection() is activated on start to plug into the standard StockSharp protective subsystems.
  • Distance and take-profit values are measured in pips. For symbols quoted with 3 or 5 decimal places the strategy multiplies the price step by 10 to match MetaTrader pip semantics.
  • There is no automatic stop-loss; risk must be controlled through the chosen parameters and external portfolio limits.

Parameters

  • Candle Type – Data type used for candle subscription.
  • MA Length – Period of the moving average.
  • MA Type – Moving average smoothing method.
  • Price Source – Candle price used to calculate the moving average.
  • Distance (pips) – Minimum pip gap between price and the moving average to trigger entries.
  • Take Profit (pips) – Profit target distance applied when only one position is open.
  • Volume Multiplier – Multiplier applied to the base volume for additional entries.
  • Base Volume – Quantity of the initial trade.
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;
	}
}