Открыть на GitHub

Стратегия BSS Triple EMA Separation

Общее описание

Стратегия BSS Triple EMA Separation — это порт эксперта MetaTrader 5 «BSS 1_0» (MQL ID 20591) на платформу StockSharp. Алгоритм отслеживает три скользящие средние с возрастающими периодами и ждёт момента, когда они разойдутся как минимум на заданную дистанцию. При выполнении условий стратегия входит в сторону преобладающего тренда, одновременно соблюдая паузу между входами и ограничение на максимальный совокупный объём позиции.

Поведение оригинального робота сохранено, все параметры вынесены в объекты StrategyParam. Комментарии в коде и документация приведены на английском языке в соответствии с требованием задания.

Логика торговли

  1. Стратегия подписывается на поток свечей, заданный параметром CandleType, и рассчитывает три скользящие средние (быструю, среднюю и медленную). Для каждой средней можно выбрать тип сглаживания (простое, экспоненциальное, сглаженное, линейно-взвешенное).
  2. Условия для покупки на закрывшейся свече:
    • Медленная MA – Средняя MA >= MinimumDistance.
    • Средняя MA – Быстрая MA >= MinimumDistance.
  3. Условия для продажи зеркальны:
    • Быстрая MA – Средняя MA >= MinimumDistance.
    • Средняя MA – Медленная MA >= MinimumDistance.
  4. Перед открытием сделки проверяется, что:
    • Все индикаторы сформированы, стратегия готова и разрешена к торговле (IsFormedAndOnlineAndAllowTrading).
    • С момента последнего входа прошло не менее MinimumPauseSeconds секунд.
    • Добавление нового лота не превысит лимит MaxPositions.
  5. При возникновении сигнала сначала закрываются сделки противоположного направления. Это повторяет поведение исходного советника, который сначала ликвидировал существующие позиции и только затем открывал новые в другом направлении.
  6. После открытия или донаращивания позиции фиксируется время сделки, чтобы соблюсти интервал между входами.

Стоп-лосс и тейк-профит не используются — риск контролируется дистанцией между средними, паузой между входами и ограничением по количеству лотов.

Параметры

Параметр Значение по умолчанию Описание
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.
  • Прежде чем открыть новую сделку, стратегия закрывает противоположные позиции, поэтому чистая позиция никогда не меняет знак без перехода через ноль.

Рекомендации по использованию

  1. Подберите значение MinimumDistance в соответствии с шагом цены инструмента:
    • EURUSD (5 знаков): 0.0005 соответствует 5 пунктам.
    • USDJPY (3 знака): 0.05 соответствует 5 пунктам.
  2. Настройте периоды и типы средних под выбранный таймфрейм и рыночную фазу.
  3. На старших таймфреймах увеличьте MinimumPauseSeconds, чтобы избежать избыточных сделок; на младших таймфреймах паузу можно уменьшить.
  4. Совместно с параметром 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;
	}

}