View on GitHub

Gandalf PRO Strategy

This strategy replicates the Gandalf PRO expert advisor from MQL. It computes two moving averages (LWMA and SMA) on the candle close and feeds them into the original recursive smoothing logic to forecast a future target price. When the projected target moves far enough away from the current close (15 price steps or more), the strategy opens a market order and stores stop-loss and take-profit levels derived from the forecast.

Long trades require the smoothed target to be above the current close by at least 15 steps; short trades require the target to be below the close by the same margin. Stop losses are defined in price steps and converted using the security price step. Take-profit levels are equal to the projected target and are monitored on every finished candle. The risk multipliers rescale the base strategy volume, enabling simple money management rules.

Parameters

  • Candle Type
  • Enable Buy
  • Buy Length
  • Buy Price Factor
  • Buy Trend Factor
  • Buy Stop Loss
  • Buy Risk Multiplier
  • Enable Sell
  • Sell Length
  • Sell Price Factor
  • Sell Trend Factor
  • Sell Stop Loss
  • Sell Risk Multiplier
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>
/// Gandalf PRO strategy converted from MQL.
/// Calculates dynamic targets using LWMA/SMA smoothing filters and
/// trades when the projected level is sufficiently far from the current price.
/// </summary>
public class GandalfProStrategy : Strategy
{
	private readonly StrategyParam<decimal> _entryBufferSteps;

	private readonly StrategyParam<bool> _enableBuy;
	private readonly StrategyParam<int> _buyLength;
	private readonly StrategyParam<decimal> _buyPriceFactor;
	private readonly StrategyParam<decimal> _buyTrendFactor;
	private readonly StrategyParam<int> _buyStopLoss;
	private readonly StrategyParam<decimal> _buyRiskMultiplier;

	private readonly StrategyParam<bool> _enableSell;
	private readonly StrategyParam<int> _sellLength;
	private readonly StrategyParam<decimal> _sellPriceFactor;
	private readonly StrategyParam<decimal> _sellTrendFactor;
	private readonly StrategyParam<int> _sellStopLoss;
	private readonly StrategyParam<decimal> _sellRiskMultiplier;

	private readonly StrategyParam<DataType> _candleType;

	private WeightedMovingAverage _buyWeighted = null!;
	private SimpleMovingAverage _buySimple = null!;
	private WeightedMovingAverage _sellWeighted = null!;
	private SimpleMovingAverage _sellSimple = null!;

	private decimal[] _closeHistory = Array.Empty<decimal>();
	private int _availableHistory;
	private int _maxPeriod;

	private decimal _prevBuyWeighted;
	private decimal _prevBuySimple;
	private bool _hasPrevBuyValues;

	private decimal _prevSellWeighted;
	private decimal _prevSellSimple;
	private bool _hasPrevSellValues;

	private decimal? _longStopPrice;
	private decimal? _longTargetPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTargetPrice;

	private decimal _priceStep;

	/// <summary>
	/// Entry buffer distance in price steps.
	/// </summary>
	public decimal EntryBufferSteps
	{
		get => _entryBufferSteps.Value;
		set => _entryBufferSteps.Value = value;
	}

	/// <summary>
	/// Enable buy logic.
	/// </summary>
	public bool EnableBuy
	{
		get => _enableBuy.Value;
		set => _enableBuy.Value = value;
	}

	/// <summary>
	/// LWMA/SMA length for buys.
	/// </summary>
	public int BuyLength
	{
		get => _buyLength.Value;
		set => _buyLength.Value = value;
	}

	/// <summary>
	/// Price smoothing factor for buys.
	/// </summary>
	public decimal BuyPriceFactor
	{
		get => _buyPriceFactor.Value;
		set => _buyPriceFactor.Value = value;
	}

	/// <summary>
	/// Trend smoothing factor for buys.
	/// </summary>
	public decimal BuyTrendFactor
	{
		get => _buyTrendFactor.Value;
		set => _buyTrendFactor.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for long trades in price steps.
	/// </summary>
	public int BuyStopLoss
	{
		get => _buyStopLoss.Value;
		set => _buyStopLoss.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the strategy volume for longs.
	/// </summary>
	public decimal BuyRiskMultiplier
	{
		get => _buyRiskMultiplier.Value;
		set => _buyRiskMultiplier.Value = value;
	}

	/// <summary>
	/// Enable sell logic.
	/// </summary>
	public bool EnableSell
	{
		get => _enableSell.Value;
		set => _enableSell.Value = value;
	}

	/// <summary>
	/// LWMA/SMA length for sells.
	/// </summary>
	public int SellLength
	{
		get => _sellLength.Value;
		set => _sellLength.Value = value;
	}

	/// <summary>
	/// Price smoothing factor for sells.
	/// </summary>
	public decimal SellPriceFactor
	{
		get => _sellPriceFactor.Value;
		set => _sellPriceFactor.Value = value;
	}

	/// <summary>
	/// Trend smoothing factor for sells.
	/// </summary>
	public decimal SellTrendFactor
	{
		get => _sellTrendFactor.Value;
		set => _sellTrendFactor.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for short trades in price steps.
	/// </summary>
	public int SellStopLoss
	{
		get => _sellStopLoss.Value;
		set => _sellStopLoss.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the strategy volume for shorts.
	/// </summary>
	public decimal SellRiskMultiplier
	{
		get => _sellRiskMultiplier.Value;
		set => _sellRiskMultiplier.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public GandalfProStrategy()
	{
		_entryBufferSteps = Param(nameof(EntryBufferSteps), 150m)
			.SetNotNegative()
			.SetDisplay("Entry Buffer", "Buffer distance in price steps", "General");

		_enableBuy = Param(nameof(EnableBuy), true)
			.SetDisplay("Enable Buy", "Allow long trades", "General");

		_buyLength = Param(nameof(BuyLength), 24)
			.SetGreaterThanZero()
			.SetDisplay("Buy Length", "LWMA/SMA length for longs", "General")
			
			.SetOptimize(5, 60, 1);

		_buyPriceFactor = Param(nameof(BuyPriceFactor), 0.18m)
			.SetRange(0m, 1m)
			.SetDisplay("Buy Price Factor", "Recursive smoothing weight for price", "General")
			
			.SetOptimize(0.05m, 0.5m, 0.01m);

		_buyTrendFactor = Param(nameof(BuyTrendFactor), 0.18m)
			.SetRange(0m, 1m)
			.SetDisplay("Buy Trend Factor", "Recursive smoothing weight for trend", "General")
			
			.SetOptimize(0.05m, 0.5m, 0.01m);

		_buyStopLoss = Param(nameof(BuyStopLoss), 62)
			.SetNotNegative()
			.SetDisplay("Buy Stop Loss", "Stop distance for longs in price steps", "Risk");

		_buyRiskMultiplier = Param(nameof(BuyRiskMultiplier), 0m)
			.SetNotNegative()
			.SetDisplay("Buy Risk Multiplier", "Volume multiplier for longs (0 = use base volume)", "Risk");

		_enableSell = Param(nameof(EnableSell), true)
			.SetDisplay("Enable Sell", "Allow short trades", "General");

		_sellLength = Param(nameof(SellLength), 24)
			.SetGreaterThanZero()
			.SetDisplay("Sell Length", "LWMA/SMA length for shorts", "General")
			
			.SetOptimize(5, 60, 1);

		_sellPriceFactor = Param(nameof(SellPriceFactor), 0.18m)
			.SetRange(0m, 1m)
			.SetDisplay("Sell Price Factor", "Recursive smoothing weight for price", "General")
			
			.SetOptimize(0.05m, 0.5m, 0.01m);

		_sellTrendFactor = Param(nameof(SellTrendFactor), 0.18m)
			.SetRange(0m, 1m)
			.SetDisplay("Sell Trend Factor", "Recursive smoothing weight for trend", "General")
			
			.SetOptimize(0.05m, 0.5m, 0.01m);

		_sellStopLoss = Param(nameof(SellStopLoss), 62)
			.SetNotNegative()
			.SetDisplay("Sell Stop Loss", "Stop distance for shorts in price steps", "Risk");

		_sellRiskMultiplier = Param(nameof(SellRiskMultiplier), 0m)
			.SetNotNegative()
			.SetDisplay("Sell Risk Multiplier", "Volume multiplier for shorts (0 = use base volume)", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Data type used for calculations", "General");
	}

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

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

		_buyWeighted?.Reset();
		_buySimple?.Reset();
		_sellWeighted?.Reset();
		_sellSimple?.Reset();

		_closeHistory = Array.Empty<decimal>();
		_availableHistory = 0;
		_maxPeriod = 0;

		_prevBuyWeighted = 0m;
		_prevBuySimple = 0m;
		_hasPrevBuyValues = false;

		_prevSellWeighted = 0m;
		_prevSellSimple = 0m;
		_hasPrevSellValues = false;

		_longStopPrice = null;
		_longTargetPrice = null;
		_shortStopPrice = null;
		_shortTargetPrice = null;

		_priceStep = 1m;
	}

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

		_priceStep = Security?.PriceStep ?? 1m;

		_maxPeriod = Math.Max(BuyLength, SellLength);
		_closeHistory = new decimal[_maxPeriod + 2];
		_availableHistory = 0;

		_buyWeighted = new WeightedMovingAverage
		{
			Length = BuyLength
		};

		_buySimple = new SMA
		{
			Length = BuyLength
		};

		_sellWeighted = new WeightedMovingAverage
		{
			Length = SellLength
		};

		_sellSimple = new SMA
		{
			Length = SellLength
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_buyWeighted, _buySimple, _sellWeighted, _sellSimple, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal buyWeightedValue, decimal buySimpleValue, decimal sellWeightedValue, decimal sellSimpleValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		ManageOpenPositions(candle);

		var buyReady = _hasPrevBuyValues && _availableHistory >= BuyLength;
		var sellReady = _hasPrevSellValues && _availableHistory >= SellLength;

		if (EnableBuy && buyReady && IsFormedAndOnlineAndAllowTrading())
		{
			var target = CalculateTarget(BuyLength, BuyPriceFactor, BuyTrendFactor, _prevBuyWeighted, _prevBuySimple);
			var entryPrice = candle.ClosePrice;

			if (target > entryPrice + EntryBufferSteps * _priceStep)
			{
				var volume = GetOrderVolume(BuyRiskMultiplier);

				if (volume > 0)
				{
					BuyMarket(volume);
					_longTargetPrice = target;
					_longStopPrice = BuyStopLoss > 0 ? entryPrice - BuyStopLoss * _priceStep : null;
				}
			}
		}

		if (EnableSell && sellReady && IsFormedAndOnlineAndAllowTrading())
		{
			var target = CalculateTarget(SellLength, SellPriceFactor, SellTrendFactor, _prevSellWeighted, _prevSellSimple);
			var entryPrice = candle.ClosePrice;

			if (target < entryPrice - EntryBufferSteps * _priceStep)
			{
				var volume = GetOrderVolume(SellRiskMultiplier);

				if (volume > 0)
				{
					SellMarket(volume);
					_shortTargetPrice = target;
					_shortStopPrice = SellStopLoss > 0 ? entryPrice + SellStopLoss * _priceStep : null;
				}
			}
		}

		if (_buyWeighted.IsFormed && _buySimple.IsFormed)
		{
			_prevBuyWeighted = buyWeightedValue;
			_prevBuySimple = buySimpleValue;
			_hasPrevBuyValues = true;
		}

		if (_sellWeighted.IsFormed && _sellSimple.IsFormed)
		{
			_prevSellWeighted = sellWeightedValue;
			_prevSellSimple = sellSimpleValue;
			_hasPrevSellValues = true;
		}

		UpdateCloseHistory(candle.ClosePrice);
	}

	private void ManageOpenPositions(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if ((_longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value) ||
				(_longTargetPrice.HasValue && candle.HighPrice >= _longTargetPrice.Value))
			{
				SellMarket(Math.Abs(Position));
				_longStopPrice = null;
				_longTargetPrice = null;
			}
		}
		else
		{
			_longStopPrice = null;
			_longTargetPrice = null;
		}

		if (Position < 0)
		{
			if ((_shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value) ||
				(_shortTargetPrice.HasValue && candle.LowPrice <= _shortTargetPrice.Value))
			{
				BuyMarket(Math.Abs(Position));
				_shortStopPrice = null;
				_shortTargetPrice = null;
			}
		}
		else
		{
			_shortStopPrice = null;
			_shortTargetPrice = null;
		}
	}

	private void UpdateCloseHistory(decimal close)
	{
		if (_closeHistory.Length <= 2)
			return;

		for (var i = _closeHistory.Length - 1; i > 1; i--)
			_closeHistory[i] = _closeHistory[i - 1];

		_closeHistory[1] = close;

		if (_availableHistory < _closeHistory.Length - 1)
			_availableHistory++;
	}

	private decimal CalculateTarget(int length, decimal priceFactor, decimal trendFactor, decimal weightedPrev, decimal simplePrev)
	{
		if (length <= 1)
			return 0m;

		var t = new decimal[length + 2];
		var s = new decimal[length + 2];

		var lengthMinusOne = length - 1m;

		var trendComponent = (6m * weightedPrev - 6m * simplePrev) / lengthMinusOne;
		t[length] = trendComponent;
		s[length] = 4m * simplePrev - 3m * weightedPrev - trendComponent;

		for (var k = length - 1; k > 0; k--)
		{
			var close = _closeHistory[k];
			s[k] = priceFactor * close + (1m - priceFactor) * (s[k + 1] + t[k + 1]);
			t[k] = trendFactor * (s[k] - s[k + 1]) + (1m - trendFactor) * t[k + 1];
		}

		return s[1] + t[1];
	}

	private decimal GetOrderVolume(decimal riskMultiplier)
	{
		var baseVolume = Volume;
		if (riskMultiplier <= 0m)
			return baseVolume;

		return baseVolume * riskMultiplier;
	}
}