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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import WeightedMovingAverage, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class gandalf_pro_strategy(Strategy):
def __init__(self):
super(gandalf_pro_strategy, self).__init__()
self._entry_buffer_steps = self.Param("EntryBufferSteps", 150.0)
self._enable_buy = self.Param("EnableBuy", True)
self._buy_length = self.Param("BuyLength", 24)
self._buy_price_factor = self.Param("BuyPriceFactor", 0.18)
self._buy_trend_factor = self.Param("BuyTrendFactor", 0.18)
self._buy_stop_loss = self.Param("BuyStopLoss", 62)
self._buy_risk_multiplier = self.Param("BuyRiskMultiplier", 0.0)
self._enable_sell = self.Param("EnableSell", True)
self._sell_length = self.Param("SellLength", 24)
self._sell_price_factor = self.Param("SellPriceFactor", 0.18)
self._sell_trend_factor = self.Param("SellTrendFactor", 0.18)
self._sell_stop_loss = self.Param("SellStopLoss", 62)
self._sell_risk_multiplier = self.Param("SellRiskMultiplier", 0.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._close_history = []
self._available_history = 0
self._max_period = 0
self._prev_buy_w = 0.0
self._prev_buy_s = 0.0
self._has_prev_buy = False
self._prev_sell_w = 0.0
self._prev_sell_s = 0.0
self._has_prev_sell = False
self._long_stop = None
self._long_target = None
self._short_stop = None
self._short_target = None
self._price_step = 1.0
self._buy_w_ind = None
self._buy_s_ind = None
self._sell_w_ind = None
self._sell_s_ind = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(gandalf_pro_strategy, self).OnReseted()
self._close_history = []
self._available_history = 0
self._max_period = 0
self._prev_buy_w = 0.0
self._prev_buy_s = 0.0
self._has_prev_buy = False
self._prev_sell_w = 0.0
self._prev_sell_s = 0.0
self._has_prev_sell = False
self._long_stop = None
self._long_target = None
self._short_stop = None
self._short_target = None
self._price_step = 1.0
def OnStarted2(self, time):
super(gandalf_pro_strategy, self).OnStarted2(time)
ps = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps <= 0:
ps = 1.0
self._price_step = ps
buy_len = int(self._buy_length.Value)
sell_len = int(self._sell_length.Value)
self._max_period = max(buy_len, sell_len)
self._close_history = [0.0] * (self._max_period + 2)
self._available_history = 0
self._buy_w_ind = WeightedMovingAverage()
self._buy_w_ind.Length = buy_len
self._buy_s_ind = SimpleMovingAverage()
self._buy_s_ind.Length = buy_len
self._sell_w_ind = WeightedMovingAverage()
self._sell_w_ind.Length = sell_len
self._sell_s_ind = SimpleMovingAverage()
self._sell_s_ind.Length = sell_len
self._prev_buy_w = 0.0
self._prev_buy_s = 0.0
self._has_prev_buy = False
self._prev_sell_w = 0.0
self._prev_sell_s = 0.0
self._has_prev_sell = False
self._long_stop = None
self._long_target = None
self._short_stop = None
self._short_target = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._buy_w_ind, self._buy_s_ind, self._sell_w_ind, self._sell_s_ind, self._process_candle).Start()
def _process_candle(self, candle, buy_w_val, buy_s_val, sell_w_val, sell_s_val):
if candle.State != CandleStates.Finished:
return
buy_w = float(buy_w_val)
buy_s = float(buy_s_val)
sell_w = float(sell_w_val)
sell_s = float(sell_s_val)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
# Manage open positions
pos = float(self.Position)
if pos > 0:
if ((self._long_stop is not None and low <= self._long_stop) or
(self._long_target is not None and high >= self._long_target)):
self.SellMarket(abs(pos))
self._long_stop = None
self._long_target = None
else:
self._long_stop = None
self._long_target = None
pos = float(self.Position)
if pos < 0:
if ((self._short_stop is not None and high >= self._short_stop) or
(self._short_target is not None and low <= self._short_target)):
self.BuyMarket(abs(pos))
self._short_stop = None
self._short_target = None
else:
self._short_stop = None
self._short_target = None
buy_length = int(self._buy_length.Value)
sell_length = int(self._sell_length.Value)
buf = float(self._entry_buffer_steps.Value)
buy_ready = self._has_prev_buy and self._available_history >= buy_length
sell_ready = self._has_prev_sell and self._available_history >= sell_length
if self._enable_buy.Value and buy_ready and self.IsFormedAndOnlineAndAllowTrading():
target = self._calc_target(buy_length, float(self._buy_price_factor.Value),
float(self._buy_trend_factor.Value), self._prev_buy_w, self._prev_buy_s)
if target > close + buf * self._price_step:
volume = self._get_order_volume(float(self._buy_risk_multiplier.Value))
if volume > 0:
self.BuyMarket(volume)
self._long_target = target
sl = int(self._buy_stop_loss.Value)
self._long_stop = close - sl * self._price_step if sl > 0 else None
if self._enable_sell.Value and sell_ready and self.IsFormedAndOnlineAndAllowTrading():
target = self._calc_target(sell_length, float(self._sell_price_factor.Value),
float(self._sell_trend_factor.Value), self._prev_sell_w, self._prev_sell_s)
if target < close - buf * self._price_step:
volume = self._get_order_volume(float(self._sell_risk_multiplier.Value))
if volume > 0:
self.SellMarket(volume)
self._short_target = target
sl = int(self._sell_stop_loss.Value)
self._short_stop = close + sl * self._price_step if sl > 0 else None
if self._buy_w_ind.IsFormed and self._buy_s_ind.IsFormed:
self._prev_buy_w = buy_w
self._prev_buy_s = buy_s
self._has_prev_buy = True
if self._sell_w_ind.IsFormed and self._sell_s_ind.IsFormed:
self._prev_sell_w = sell_w
self._prev_sell_s = sell_s
self._has_prev_sell = True
self._update_close_history(close)
def _update_close_history(self, close):
if len(self._close_history) <= 2:
return
i = len(self._close_history) - 1
while i > 1:
self._close_history[i] = self._close_history[i - 1]
i -= 1
self._close_history[1] = close
if self._available_history < len(self._close_history) - 1:
self._available_history += 1
def _calc_target(self, length, price_factor, trend_factor, w_prev, s_prev):
if length <= 1:
return 0.0
t = [0.0] * (length + 2)
s = [0.0] * (length + 2)
lm1 = float(length - 1)
trend_comp = (6.0 * w_prev - 6.0 * s_prev) / lm1
t[length] = trend_comp
s[length] = 4.0 * s_prev - 3.0 * w_prev - trend_comp
for k in range(length - 1, 0, -1):
c = self._close_history[k]
s[k] = price_factor * c + (1.0 - price_factor) * (s[k + 1] + t[k + 1])
t[k] = trend_factor * (s[k] - s[k + 1]) + (1.0 - trend_factor) * t[k + 1]
return s[1] + t[1]
def _get_order_volume(self, risk_multiplier):
base_vol = float(self.Volume)
if risk_multiplier <= 0:
return base_vol
return base_vol * risk_multiplier
def CreateClone(self):
return gandalf_pro_strategy()