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
Subscribe to the timeframe selected by CandleType (default: 1-hour candles) and process only completed candles.
Maintain a rolling history of closing prices large enough to cover the maximum of CountBuy and CountSell plus one extra bar.
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].
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.
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.
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];
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange
class gandalf_pro_projection_strategy(Strategy):
def __init__(self):
super(gandalf_pro_projection_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._filter_length = self.Param("FilterLength", 24) \
.SetDisplay("Filter Length", "Smoothing filter length", "Filter")
self._price_factor = self.Param("PriceFactor", 0.18) \
.SetDisplay("Price Factor", "Close price weight in filter", "Filter")
self._trend_factor = self.Param("TrendFactor", 0.18) \
.SetDisplay("Trend Factor", "Trend term weight in filter", "Filter")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period for entry buffer", "Indicators")
self._close_buffer = []
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def FilterLength(self):
return self._filter_length.Value
@property
def PriceFactor(self):
return self._price_factor.Value
@property
def TrendFactor(self):
return self._trend_factor.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(gandalf_pro_projection_strategy, self).OnStarted2(time)
self._close_buffer = []
self._entry_price = 0.0
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
av = float(atr_val)
close = float(candle.ClosePrice)
self._close_buffer.append(close)
max_depth = self.FilterLength + 2
while len(self._close_buffer) > max_depth:
self._close_buffer.pop(0)
fl = self.FilterLength
if len(self._close_buffer) <= fl or av <= 0:
return
target = self._calculate_target()
if target is None:
return
buffer_dist = av * 0.3
# Manage position
if self.Position > 0:
if target < close - buffer_dist:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if target > close + buffer_dist:
self.BuyMarket()
self._entry_price = 0.0
# Entry
if self.Position == 0:
if target > close + buffer_dist:
self._entry_price = close
self.BuyMarket()
elif target < close - buffer_dist:
self._entry_price = close
self.SellMarket()
def _calculate_target(self):
n = self.FilterLength
if n < 2 or len(self._close_buffer) < n + 1:
return None
total = 0.0
for i in range(1, n + 1):
total += self._get_close(i)
sm = total / n
weighted_sum = 0.0
for i in range(n):
price = self._get_close(i + 1)
weight = n - i
weighted_sum += price * weight
denominator = n * (n + 1) / 2.0
if denominator <= 0:
return None
lm = weighted_sum / denominator
divisor = n - 1
if divisor <= 0:
return None
pf = float(self.PriceFactor)
tf = float(self.TrendFactor)
s = [0.0] * (n + 2)
t = [0.0] * (n + 2)
tn = (6.0 * lm - 6.0 * sm) / divisor
sn = 4.0 * sm - 3.0 * lm - tn
s[n] = sn
t[n] = tn
for k in range(n - 1, 0, -1):
c = self._get_close(k)
s[k] = pf * c + (1.0 - pf) * (s[k + 1] + t[k + 1])
t[k] = tf * (s[k] - s[k + 1]) + (1.0 - tf) * t[k + 1]
return s[1] + t[1]
def _get_close(self, index):
idx = len(self._close_buffer) - 1 - index
if idx < 0:
idx = 0
if idx >= len(self._close_buffer):
idx = len(self._close_buffer) - 1
return self._close_buffer[idx]
def OnReseted(self):
super(gandalf_pro_projection_strategy, self).OnReseted()
self._close_buffer = []
self._entry_price = 0.0
def CreateClone(self):
return gandalf_pro_projection_strategy()