Gandalf PRO — это порт на StockSharp советника MetaTrader 4 Gandalf_PRO. Исходный робот строит адаптивный сглаживающий фильтр
на основе линейно-взвешенной средней и рекурсивной трендовой составляющей. Когда прогнозируемая цена уходит минимум на 15 пунктов
от текущего рынка, советник открывает позицию в соответствующем направлении, фиксируя тейк-профит на уровне прогноза и выставляя
дальний стоп-лосс. Версия для StockSharp воссоздаёт тот же фильтр и логику сигналов, используя высокоуровневое API свечей, поэтому
все расчёты выполняются только по завершённым барам.
Логика торговли
Подписаться на таймфрейм, указанный в CandleType (по умолчанию часовые свечи), и обрабатывать только завершённые свечи.
Поддерживать скользящее окно закрытий, достаточное для максимума из CountBuy и CountSell плюс одна дополнительная свеча.
Воссоздать функцию Out() из MQL: вычислить линейно-взвешенную и простую скользящие средние со сдвигом на один бар, получить
рекурсивные компоненты s и t с учётом весов цены и тренда, после чего получить прогноз s[1] + t[1].
Для длинных сигналов (EnableBuy):
Проверить, что прогноз лежит как минимум на 15 пунктов выше последнего закрытия (Bid + 15*x*Point в MT4).
Если длинная позиция отсутствует, открыть покупку с объёмом, рассчитанным через BaseVolume и BuyRiskMultiplier.
Запомнить прогноз как тейк-профит и вычислить стоп-лосс, вычитая BuyStopLossPips, переведённые в абсолютную цену.
Для коротких сигналов (EnableSell):
Требуется, чтобы прогноз находился минимум на 15 пунктов ниже закрытия.
Если короткой позиции нет, открыть продажу (при необходимости сначала перевернув текущий лонг).
Сохранить прогноз как тейк-профит и выставить стоп-лосс на SellStopLossPips пунктов выше рынка.
При наличии позиции отслеживать каждую завершённую свечу:
Закрывать лонг, если минимум свечи пробивает стоп или максимум достигает тейк-профита.
Закрывать шорт, если максимум свечи пробивает стоп или минимум достигает цели.
Для выхода вызывается ClosePosition(), что закрывает нетто-позицию в StockSharp.
Параметры
Название
Тип
Значение по умолчанию
Описание
EnableBuy
bool
true
Разрешает открытие длинных позиций.
CountBuy
int
24
Длина фильтра сглаживания для длинных прогнозов.
BuyPriceFactor
decimal
0.18
Вес текущего закрытия в длинном рекурсивном фильтре.
BuyTrendFactor
decimal
0.18
Вес трендовой составляющей в длинном фильтре.
BuyStopLossPips
int
62
Размер стоп-лосса для покупок в пунктах.
BuyRiskMultiplier
decimal
0
Множитель к BaseVolume перед отправкой длинного ордера (0 оставляет базовый объём).
EnableSell
bool
true
Разрешает открытие коротких позиций.
CountSell
int
24
Длина фильтра сглаживания для коротких прогнозов.
SellPriceFactor
decimal
0.18
Вес текущего закрытия в коротком рекурсивном фильтре.
SellTrendFactor
decimal
0.18
Вес трендовой составляющей в коротком фильтре.
SellStopLossPips
int
62
Размер стоп-лосса для продаж в пунктах.
SellRiskMultiplier
decimal
0
Множитель к BaseVolume перед отправкой короткого ордера (0 оставляет базовый объём).
BaseVolume
decimal
1
Базовый объём заявок, используемый при нулевых множителях риска.
CandleType
DataType
часовой таймфрейм
Ряд свечей, который обрабатывает стратегия.
Отличия от оригинального советника MetaTrader
В MetaTrader допускается одновременное наличие buy и sell ордеров с разными magic number. В StockSharp применяется неттинговая
модель, поэтому перед открытием противоположной позиции стратегия закрывает или переворачивает текущую.
Функция расчёта лота в MT4 опиралась на свободную маржу. В порте введены BaseVolume и два множителя риска: при нуле берётся
базовый объём, при положительном значении объём просто масштабируется (BaseVolume * RiskMultiplier).
Стоп-лосс и тейк-профит исполняются при анализе завершённых свечей. Внутрибарные исполнения могут отличаться от MT4, где
защитные ордера обрабатываются брокером.
Коррекция на пятизначные котировки (Digits, Point) реализована через Security.Decimals и Security.PriceStep, что позволяет
переводить пункты в абсолютные цены.
Все расчёты выполняются внутри C# без вызовов iMA: рекурсивный фильтр реализован в методе CalculateTarget с теми же
коэффициентами, что и в MQL.
Рекомендации по использованию
Перед запуском назначьте инструмент в Strategy.Security. Если инструмент не выбран, стратегия выбросит исключение.
Настройте BaseVolume под контракт вашего брокера. Множители риска используйте только для пропорционального увеличения объёма.
Для генерации сигналов необходима история как минимум max(CountBuy, CountSell) + 1 свечей. Позаботьтесь о прогреве или
загрузке исторических данных перед запуском.
Буфер в 15 пунктов фиксирован (как и в MT4). Повышайте значения CountBuy/CountSell или корректируйте веса цены и тренда,
чтобы подстроить поведение под исходный советник.
Поскольку выходы зависят от экстремумов свечей, подбирайте таймфрейм с учётом задержек исполнения. На низких таймфреймах реакции
происходят быстрее, но возрастает количество сделок и требования к истории.
Особенности реализации
Используется SubscribeCandles() и привязка Bind(ProcessCandle), чтобы решения принимались только по завершённым свечам.
Хранится компактный список последних закрытий, по которому при каждом вызове заново строятся рекурсивные последовательности s
и t, полностью копирующие функцию Out().
Перевод пунктов в абсолютные значения происходит через шаг цены инструмента, что повторяет вычисление x * Point в MQL.
При достижении защитных уровней вызывается ClosePosition(), благодаря чему нетто-позиция закрывается до появления нового
сигнала.
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()