View on GitHub

Gandalf PRO Projection Strategy

Overview

The Gandalf PRO strategy is a StockSharp port of the MetaTrader 4 expert advisor Gandalf_PRO. The original robot builds an adaptive smoothing filter from a weighted moving average and a recursive trend component. When the projected price moves at least 15 pips beyond the current market price, the EA enters in that direction with a distant stop-loss and a take-profit at the projected level. The StockSharp conversion reproduces the same filter and decision logic while relying on the high-level candle API so every calculation is performed on finished bars.

Trading logic

  1. Subscribe to the timeframe selected by CandleType (default: 1-hour candles) and process only completed candles.
  2. Maintain a rolling history of closing prices large enough to cover the maximum of CountBuy and CountSell plus one extra bar.
  3. Recreate the MetaTrader Out() function: compute linear-weighted and simple moving averages (using a one-bar shift), derive the recursive s and t components with the configured price and trend factors, and obtain the projected price s[1] + t[1].
  4. For long setups (EnableBuy):
    • Check that the projected price is at least 15 pips above the latest close (Bid + 15*x*Point in MT4).
    • If no long position is open, buy the configured volume (see BaseVolume and BuyRiskMultiplier).
    • Store the projected price as take-profit and compute the stop-loss by subtracting BuyStopLossPips converted to price steps.
  5. For short setups (EnableSell):
    • Require the projected price to sit at least 15 pips below the last close.
    • If no short position is open, sell the configured volume (reversing an existing long if necessary).
    • Save the projected price as take-profit and set the stop-loss SellStopLossPips pips above the market.
  6. While a position exists, monitor every finished candle:
    • Exit longs if the candle low crosses the stored stop or the high reaches the take-profit.
    • Exit shorts if the candle high crosses the stop or the low hits the target.
    • Exits use ClosePosition() which flattens the net exposure in StockSharp.

Parameters

Name Type Default Description
EnableBuy bool true Allow the strategy to open long positions.
CountBuy int 24 Length of the smoothing filter used for long projections.
BuyPriceFactor decimal 0.18 Weight of the current close in the long recursive filter.
BuyTrendFactor decimal 0.18 Weight applied to the trend term when building the long projection.
BuyStopLossPips int 62 Stop-loss distance for long positions, measured in pips.
BuyRiskMultiplier decimal 0 Multiplier applied to BaseVolume before sending a long order (0 keeps the base volume).
EnableSell bool true Allow the strategy to open short positions.
CountSell int 24 Length of the smoothing filter used for short projections.
SellPriceFactor decimal 0.18 Weight of the current close in the short recursive filter.
SellTrendFactor decimal 0.18 Weight applied to the trend term when building the short projection.
SellStopLossPips int 62 Stop-loss distance for short positions, measured in pips.
SellRiskMultiplier decimal 0 Multiplier applied to BaseVolume before sending a short order (0 keeps the base volume).
BaseVolume decimal 1 Base order size used when both risk multipliers are zero.
CandleType DataType 1-hour time frame Candle series processed by the strategy.

Differences from the original MetaTrader EA

  • MetaTrader can hold independent buy and sell tickets simultaneously. StockSharp uses net positions, so the port closes or reverses an existing position before opening the opposite side.
  • The MT4 lot function used account free margin. The conversion exposes BaseVolume and two risk multipliers; when they are zero the base volume is used as-is, otherwise the volume is simply scaled (BaseVolume * RiskMultiplier).
  • Stop-loss and take-profit levels are executed by monitoring completed candles. Intrabar fills may therefore differ from MetaTrader where protective orders are managed by the broker.
  • The five-digit Digits/Point adjustment is emulated by inspecting Security.Decimals and Security.PriceStep to convert pip distances into absolute prices.
  • All indicator calculations are performed in managed code without calling iMA; the recursive filter is recreated in CalculateTarget using the same coefficients as the MQL function.

Usage notes

  • Assign the desired instrument to Strategy.Security before starting. The strategy throws an exception if no security is attached.
  • Configure BaseVolume to match the contract size expected by your venue; adjust the risk multipliers only if you want to scale the exposure relative to the base volume.
  • The candle history must contain at least max(CountBuy, CountSell) + 1 bars before any trade can be generated. Provide sufficient warm-up data or start the strategy with historical candles loaded.
  • The 15-pip entry buffer is fixed (just like in the EA). Increase CountBuy/CountSell to smooth the projection or tweak the price/trend factors to match the behaviour observed in MetaTrader.
  • Because exits depend on candle extremes, enable a timeframe that suits your execution latency. Lower timeframes will react sooner but require more historical data and may generate more signals.

Implementation details

  • Uses SubscribeCandles() with Bind(ProcessCandle) so every decision is based on finalized candles.
  • Keeps a compact list of recent closes and rebuilds the recursive s/t filter on demand, mimicking the Out() routine.
  • Converts pip-based offsets via the instrument tick size and decimal precision to replicate the MetaTrader x * Point scaling.
  • ClosePosition() is invoked when protective levels are breached, ensuring the net position is flattened before another entry is considered.
using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Gandalf PRO trend-following strategy using adaptive smoothing filter.
/// Opens trades when projected price exceeds a buffer threshold.
/// </summary>
public class GandalfProProjectionStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _filterLength;
	private readonly StrategyParam<decimal> _priceFactor;
	private readonly StrategyParam<decimal> _trendFactor;
	private readonly StrategyParam<int> _atrLength;

	private readonly List<decimal> _closeBuffer = new();
	private decimal _entryPrice;

	public GandalfProProjectionStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_filterLength = Param(nameof(FilterLength), 24)
			.SetDisplay("Filter Length", "Smoothing filter length.", "Filter");

		_priceFactor = Param(nameof(PriceFactor), 0.18m)
			.SetDisplay("Price Factor", "Close price weight in filter.", "Filter");

		_trendFactor = Param(nameof(TrendFactor), 0.18m)
			.SetDisplay("Trend Factor", "Trend term weight in filter.", "Filter");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period for entry buffer.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int FilterLength
	{
		get => _filterLength.Value;
		set => _filterLength.Value = value;
	}

	public decimal PriceFactor
	{
		get => _priceFactor.Value;
		set => _priceFactor.Value = value;
	}

	public decimal TrendFactor
	{
		get => _trendFactor.Value;
		set => _trendFactor.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

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

		_closeBuffer.Clear();
		_entryPrice = 0;
	}

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

		var atr = new AverageTrueRange { Length = AtrLength };

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

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

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

		_closeBuffer.Add(candle.ClosePrice);
		var maxDepth = FilterLength + 2;
		while (_closeBuffer.Count > maxDepth)
			_closeBuffer.RemoveAt(0);

		if (_closeBuffer.Count <= FilterLength || atrVal <= 0)
			return;

		var close = candle.ClosePrice;
		var target = CalculateTarget();
		if (target == null)
			return;

		var targetPrice = target.Value;
		var buffer = atrVal * 0.3m;

		// Manage position
		if (Position > 0)
		{
			// Exit if projection flips below close or on stop
			if (targetPrice < close - buffer)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (targetPrice > close + buffer)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		// Entry
		if (Position == 0)
		{
			if (targetPrice > close + buffer)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (targetPrice < close - buffer)
			{
				_entryPrice = close;
				SellMarket();
			}
		}
	}

	private decimal? CalculateTarget()
	{
		var n = FilterLength;
		if (n < 2 || _closeBuffer.Count < n + 1)
			return null;

		var sum = 0m;
		for (var i = 1; i <= n; i++)
			sum += GetClose(i);

		var sm = sum / n;

		var weightedSum = 0m;
		for (var i = 0; i < n; i++)
		{
			var price = GetClose(i + 1);
			var weight = n - i;
			weightedSum += price * weight;
		}

		var denominator = (decimal)n * (n + 1) / 2m;
		if (denominator <= 0m)
			return null;

		var lm = weightedSum / denominator;
		var divisor = n - 1;
		if (divisor <= 0)
			return null;

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

		var tn = (6m * lm - 6m * sm) / divisor;
		var sn = 4m * sm - 3m * lm - tn;
		s[n] = sn;
		t[n] = tn;

		for (var k = n - 1; k > 0; k--)
		{
			var close = GetClose(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 GetClose(int index)
	{
		var idx = _closeBuffer.Count - 1 - index;
		if (idx < 0) idx = 0;
		if (idx >= _closeBuffer.Count) idx = _closeBuffer.Count - 1;
		return _closeBuffer[idx];
	}
}