Стратегия BSS Triple EMA Separation
Общее описание
Стратегия BSS Triple EMA Separation — это порт эксперта MetaTrader 5 «BSS 1_0» (MQL ID 20591) на платформу StockSharp. Алгоритм отслеживает три скользящие средние с возрастающими периодами и ждёт момента, когда они разойдутся как минимум на заданную дистанцию. При выполнении условий стратегия входит в сторону преобладающего тренда, одновременно соблюдая паузу между входами и ограничение на максимальный совокупный объём позиции.
Поведение оригинального робота сохранено, все параметры вынесены в объекты StrategyParam. Комментарии в коде и документация приведены на английском языке в соответствии с требованием задания.
Логика торговли
- Стратегия подписывается на поток свечей, заданный параметром
CandleType, и рассчитывает три скользящие средние (быструю, среднюю и медленную). Для каждой средней можно выбрать тип сглаживания (простое, экспоненциальное, сглаженное, линейно-взвешенное).
- Условия для покупки на закрывшейся свече:
Медленная MA – Средняя MA >= MinimumDistance.
Средняя MA – Быстрая MA >= MinimumDistance.
- Условия для продажи зеркальны:
Быстрая MA – Средняя MA >= MinimumDistance.
Средняя MA – Медленная MA >= MinimumDistance.
- Перед открытием сделки проверяется, что:
- Все индикаторы сформированы, стратегия готова и разрешена к торговле (
IsFormedAndOnlineAndAllowTrading).
- С момента последнего входа прошло не менее
MinimumPauseSeconds секунд.
- Добавление нового лота не превысит лимит
MaxPositions.
- При возникновении сигнала сначала закрываются сделки противоположного направления. Это повторяет поведение исходного советника, который сначала ликвидировал существующие позиции и только затем открывал новые в другом направлении.
- После открытия или донаращивания позиции фиксируется время сделки, чтобы соблюсти интервал между входами.
Стоп-лосс и тейк-профит не используются — риск контролируется дистанцией между средними, паузой между входами и ограничением по количеству лотов.
Параметры
| Параметр |
Значение по умолчанию |
Описание |
OrderVolume |
0.1 |
Объём одной заявки. Совокупная позиция ограничена произведением OrderVolume * MaxPositions. |
MaxPositions |
2 |
Максимальное количество лотов (в одном направлении), которое допускается держать одновременно. |
MinimumDistance |
0.0005 |
Минимальная ценовая дистанция между соседними средними. Значение подбирается под инструмент (для пары EURUSD с точностью 5 знаков 0.0005 соответствует 5 пунктам). |
MinimumPauseSeconds |
600 |
Пауза между новыми входами в секундах. Закрытие позиции таймер не сбрасывает — учитываются только входы. |
FirstMaPeriod |
5 |
Период быстрой скользящей средней. Должен быть строго меньше SecondMaPeriod. |
FirstMaMethod |
Exponential |
Тип сглаживания быстрой средней (Simple, Exponential, Smoothed, LinearWeighted). |
SecondMaPeriod |
25 |
Период средней скользящей средней. Должен быть строго меньше ThirdMaPeriod. |
SecondMaMethod |
Exponential |
Тип сглаживания средней средней. |
ThirdMaPeriod |
125 |
Период медленной скользящей средней. |
ThirdMaMethod |
Exponential |
Тип сглаживания медленной средней. |
CandleType |
Таймфрейм 1 минута |
Источник свечных данных для расчётов и сигналов. |
Особенности реализации
- Использован высокоуровневый API StockSharp: через
SubscribeCandles получается поток свечей, а Bind одновременно подаёт значения на индикаторы и обработчик сигналов.
- Скользящие средние создаются при запуске стратегии в соответствии с выбранными типами. Конфигурация по умолчанию соответствует оригиналу (три экспоненциальные средние по цене закрытия).
- В методе
OnStarted вызывается StartProtection(), чтобы активировать встроенный механизм контроля позиции.
- Переопределён
OnPositionChanged: при увеличении абсолютной позиции сохраняется время сделки, что позволяет реализовать паузу между входами аналогично версии на MetaTrader.
- Прежде чем открыть новую сделку, стратегия закрывает противоположные позиции, поэтому чистая позиция никогда не меняет знак без перехода через ноль.
Рекомендации по использованию
- Подберите значение
MinimumDistance в соответствии с шагом цены инструмента:
- EURUSD (5 знаков):
0.0005 соответствует 5 пунктам.
- USDJPY (3 знака):
0.05 соответствует 5 пунктам.
- Настройте периоды и типы средних под выбранный таймфрейм и рыночную фазу.
- На старших таймфреймах увеличьте
MinimumPauseSeconds, чтобы избежать избыточных сделок; на младших таймфреймах паузу можно уменьшить.
- Совместно с параметром
OrderVolume подберите MaxPositions, чтобы итоговый размер позиции соответствовал вашему риск-плану.
Ограничения относительно оригинала
- В MQL-версии можно было выбирать тип цены (open, high, low и т. д.). В данном порте используется цена закрытия, как и в стандартной конфигурации исходного эксперта.
- Стратегия работает в модели чистой позиции: положительное значение
Position соответствует лонгу, отрицательное — шорту. При достижении лимита MaxPositions дополнительные лоты не добавляются до сокращения позиции, что соответствует подсчёту открытых сделок в MetaTrader.
Следуя этим рекомендациям, вы сможете воспроизвести торговую идею BSS в инфраструктуре StockSharp и дополнить её собственными фильтрами или модулями управления рисками.
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;
public class BssTripleEmaSeparationStrategy : Strategy
{
public enum MaMethods
{
Simple,
Exponential,
Smoothed,
LinearWeighted,
}
// Small epsilon used to compare decimal volumes without floating point noise.
private readonly StrategyParam<decimal> _volumeTolerance;
// User configurable parameters.
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<decimal> _minimumDistance;
private readonly StrategyParam<int> _minimumPauseSeconds;
private readonly StrategyParam<int> _firstMaPeriod;
private readonly StrategyParam<int> _secondMaPeriod;
private readonly StrategyParam<int> _thirdMaPeriod;
private readonly StrategyParam<MaMethods> _firstMaMethod;
private readonly StrategyParam<MaMethods> _secondMaMethod;
private readonly StrategyParam<MaMethods> _thirdMaMethod;
private readonly StrategyParam<DataType> _candleType;
// Indicator instances created according to the selected parameters.
private IIndicator _firstMa = null!;
private IIndicator _secondMa = null!;
private IIndicator _thirdMa = null!;
// Timestamp of the last position entry used to enforce the pause between trades.
private DateTimeOffset? _lastEntryTime;
/// <summary>
/// Tolerance used when comparing accumulated volume values.
/// </summary>
public decimal VolumeTolerance
{
get => _volumeTolerance.Value;
set => _volumeTolerance.Value = value;
}
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
public decimal MinimumDistance
{
get => _minimumDistance.Value;
set => _minimumDistance.Value = value;
}
public int MinimumPauseSeconds
{
get => _minimumPauseSeconds.Value;
set => _minimumPauseSeconds.Value = value;
}
public int FirstMaPeriod
{
get => _firstMaPeriod.Value;
set => _firstMaPeriod.Value = value;
}
public int SecondMaPeriod
{
get => _secondMaPeriod.Value;
set => _secondMaPeriod.Value = value;
}
public int ThirdMaPeriod
{
get => _thirdMaPeriod.Value;
set => _thirdMaPeriod.Value = value;
}
public MaMethods FirstMaMethod
{
get => _firstMaMethod.Value;
set => _firstMaMethod.Value = value;
}
public MaMethods SecondMaMethod
{
get => _secondMaMethod.Value;
set => _secondMaMethod.Value = value;
}
public MaMethods ThirdMaMethod
{
get => _thirdMaMethod.Value;
set => _thirdMaMethod.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public BssTripleEmaSeparationStrategy()
{
_volumeTolerance = Param(nameof(VolumeTolerance), 1e-8m)
.SetGreaterThanZero()
.SetDisplay("Volume Tolerance", "Tolerance when comparing volume values", "Risk");
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Volume used for each entry order", "Trading");
_maxPositions = Param(nameof(MaxPositions), 2)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum simultaneous entries per direction", "Risk");
_minimumDistance = Param(nameof(MinimumDistance), 50m)
.SetGreaterThanZero()
.SetDisplay("Minimum Distance", "Minimum price gap between moving averages", "Signals");
_minimumPauseSeconds = Param(nameof(MinimumPauseSeconds), 600)
.SetNotNegative()
.SetDisplay("Minimum Pause (sec)", "Pause between new entries in seconds", "Risk");
_firstMaPeriod = Param(nameof(FirstMaPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("First MA Period", "Period for the fastest moving average", "Indicators");
_firstMaMethod = Param(nameof(FirstMaMethod), MaMethods.Exponential)
.SetDisplay("First MA Method", "Smoothing method for the fastest moving average", "Indicators");
_secondMaPeriod = Param(nameof(SecondMaPeriod), 25)
.SetGreaterThanZero()
.SetDisplay("Second MA Period", "Period for the medium moving average", "Indicators");
_secondMaMethod = Param(nameof(SecondMaMethod), MaMethods.Exponential)
.SetDisplay("Second MA Method", "Smoothing method for the medium moving average", "Indicators");
_thirdMaPeriod = Param(nameof(ThirdMaPeriod), 125)
.SetGreaterThanZero()
.SetDisplay("Third MA Period", "Period for the slowest moving average", "Indicators");
_thirdMaMethod = Param(nameof(ThirdMaMethod), MaMethods.Exponential)
.SetDisplay("Third MA Method", "Smoothing method for the slowest moving average", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_lastEntryTime = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (FirstMaPeriod >= SecondMaPeriod)
throw new InvalidOperationException("First MA period must be less than second MA period.");
if (SecondMaPeriod >= ThirdMaPeriod)
throw new InvalidOperationException("Second MA period must be less than third MA period.");
_firstMa = CreateMovingAverage(FirstMaMethod, FirstMaPeriod);
_secondMa = CreateMovingAverage(SecondMaMethod, SecondMaPeriod);
_thirdMa = CreateMovingAverage(ThirdMaMethod, ThirdMaPeriod);
_lastEntryTime = null;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_firstMa, _secondMa, _thirdMa, ProcessCandle).Start();
}
private static IIndicator CreateMovingAverage(MaMethods method, int period)
{
return method switch
{
MaMethods.Simple => new SimpleMovingAverage { Length = period },
MaMethods.Smoothed => new SmoothedMovingAverage { Length = period },
MaMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
_ => new ExponentialMovingAverage { Length = period },
};
}
private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal thirdValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_firstMa.IsFormed || !_secondMa.IsFormed || !_thirdMa.IsFormed)
return;
var minDistance = MinimumDistance;
var longSpreadOk = thirdValue - secondValue >= minDistance && secondValue - firstValue >= minDistance;
var shortSpreadOk = firstValue - secondValue >= minDistance && secondValue - thirdValue >= minDistance;
if (!longSpreadOk && !shortSpreadOk)
return;
var time = candle.OpenTime;
if (longSpreadOk)
{
if (TryCloseOppositePositions(true))
return;
if (CanEnterPosition(time, true))
{
BuyMarket(OrderVolume);
_lastEntryTime = time;
}
return;
}
if (shortSpreadOk)
{
if (TryCloseOppositePositions(false))
return;
if (CanEnterPosition(time, false))
{
SellMarket(OrderVolume);
_lastEntryTime = time;
}
}
}
private bool CanEnterPosition(DateTimeOffset time, bool isLong)
{
// Trading is allowed only when the strategy is ready, the pause elapsed, and exposure stays within bounds.
if (!IsPauseElapsed(time))
return false;
var targetPosition = Position + (isLong ? OrderVolume : -OrderVolume);
var maxExposure = MaxPositions * OrderVolume;
return Math.Abs(targetPosition) <= maxExposure + VolumeTolerance;
}
private bool IsPauseElapsed(DateTimeOffset time)
{
var pauseSeconds = MinimumPauseSeconds;
if (pauseSeconds <= 0)
return true;
if (_lastEntryTime is null)
return true;
return time - _lastEntryTime.Value >= TimeSpan.FromSeconds(pauseSeconds);
}
private bool TryCloseOppositePositions(bool isLong)
{
// Close active trades in the opposite direction before opening a new position.
if (isLong)
{
if (Position < -VolumeTolerance)
{
BuyMarket(Math.Abs(Position));
return true;
}
}
else
{
if (Position > VolumeTolerance)
{
SellMarket(Position);
return true;
}
}
return false;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class bss_triple_ema_separation_strategy(Strategy):
def __init__(self):
super(bss_triple_ema_separation_strategy, self).__init__()
self._volume_tolerance = self.Param("VolumeTolerance", 1e-8)
self._order_volume = self.Param("OrderVolume", 0.1)
self._max_positions = self.Param("MaxPositions", 2)
self._minimum_distance = self.Param("MinimumDistance", 50.0)
self._minimum_pause_seconds = self.Param("MinimumPauseSeconds", 600)
self._first_ma_period = self.Param("FirstMaPeriod", 5)
self._second_ma_period = self.Param("SecondMaPeriod", 25)
self._third_ma_period = self.Param("ThirdMaPeriod", 125)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._first_ma = None
self._second_ma = None
self._third_ma = None
self._last_entry_time = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(bss_triple_ema_separation_strategy, self).OnStarted2(time)
self._first_ma = ExponentialMovingAverage()
self._first_ma.Length = self._first_ma_period.Value
self._second_ma = ExponentialMovingAverage()
self._second_ma.Length = self._second_ma_period.Value
self._third_ma = ExponentialMovingAverage()
self._third_ma.Length = self._third_ma_period.Value
self._last_entry_time = None
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._first_ma, self._second_ma, self._third_ma, self._process_candle).Start()
def _process_candle(self, candle, first_value, second_value, third_value):
if candle.State != CandleStates.Finished:
return
if not self._first_ma.IsFormed or not self._second_ma.IsFormed or not self._third_ma.IsFormed:
return
fv = float(first_value)
sv = float(second_value)
tv = float(third_value)
min_distance = self._minimum_distance.Value
long_spread_ok = tv - sv >= min_distance and sv - fv >= min_distance
short_spread_ok = fv - sv >= min_distance and sv - tv >= min_distance
if not long_spread_ok and not short_spread_ok:
return
time = candle.OpenTime
if long_spread_ok:
if self._try_close_opposite_positions(True):
return
if self._can_enter_position(time, True):
self.BuyMarket(self._order_volume.Value)
self._last_entry_time = time
return
if short_spread_ok:
if self._try_close_opposite_positions(False):
return
if self._can_enter_position(time, False):
self.SellMarket(self._order_volume.Value)
self._last_entry_time = time
def _can_enter_position(self, time, is_long):
if not self._is_pause_elapsed(time):
return False
vol = self._order_volume.Value
target_position = self.Position + (vol if is_long else -vol)
max_exposure = self._max_positions.Value * vol
return abs(target_position) <= max_exposure + self._volume_tolerance.Value
def _is_pause_elapsed(self, time):
pause_seconds = self._minimum_pause_seconds.Value
if pause_seconds <= 0:
return True
if self._last_entry_time is None:
return True
return (time - self._last_entry_time) >= TimeSpan.FromSeconds(pause_seconds)
def _try_close_opposite_positions(self, is_long):
tol = self._volume_tolerance.Value
if is_long:
if self.Position < -tol:
self.BuyMarket(abs(self.Position))
return True
else:
if self.Position > tol:
self.SellMarket(self.Position)
return True
return False
def OnReseted(self):
super(bss_triple_ema_separation_strategy, self).OnReseted()
self._first_ma = None
self._second_ma = None
self._third_ma = None
self._last_entry_time = None
def CreateClone(self):
return bss_triple_ema_separation_strategy()