Открыть на GitHub

Стратегия Channels Envelope Cross

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

Channels Envelope Cross — это точный перенос советника MetaTrader «Channels» на платформу StockSharp. Стратегия работает на часовом таймфрейме и отслеживает двухпериодные экспоненциальные средние (EMA) по ценам открытия и закрытия относительно трёх оболочек (0.3%, 0.7% и 1.0%), построенных вокруг медленной EMA с периодом 220. Пробои быстрой EMA через границы оболочек формируют торговые сигналы, а опциональный фильтр по времени ограничивает торговлю заданным интервалом часов.

Логика работы

  1. Набор индикаторов
    • Быстрая EMA (период 2) по ценам закрытия свечи.
    • Быстрая EMA (период 2) по ценам открытия свечи.
    • Медленная EMA (период 220) по ценам закрытия.
    • Верхние и нижние границы оболочек, рассчитанные от медленной EMA с отклонениями 0.3%, 0.7% и 1.0%.
  2. Условия для покупки
    • Срабатывают, когда быстрая EMA по закрытию пробивает снизу-вверх нижние границы оболочек 1.0% или 0.7%, остаётся ниже нижней границы 0.3% две свечи подряд, пересекает медленную EMA или пробивает верхние границы 0.3%/0.7%. Любое из условий открывает длинную позицию при отсутствии открытых сделок.
  3. Условия для продажи
    • Срабатывают, когда быстрая EMA по открытию пробивает сверху-вниз любую из верхних оболочек, опускается ниже медленной EMA или пересекает нижние границы сверху-вниз. Любое из условий открывает короткую позицию, если нет активной позиции.
  4. Управление рисками
    • Фиксированные стоп-лоссы и тейк-профиты задаются в пунктах отдельно для лонгов и шортов. Значение «0» отключает соответствующий уровень.
    • Независимые трейлинг-стопы для длинных и коротких позиций подтягивают защитный стоп, когда прибыль превышает расстояние трейлинга плюс минимальный шаг.
  5. Фильтр по времени
    • При включении стратегиия проверяет час открытия свечи и допускает новые входы только в пределах указанного диапазона (включительно). Управление открытыми позициями продолжается всегда.

Параметры

Параметр Значение
OrderVolume Объём сделки (лоты/контракты).
UseTradeHours Включает фильтр торговых часов.
FromHour / ToHour Начальный и конечный час торгового окна (поддерживается переход через полночь).
StopLossBuyPips / StopLossSellPips Размер стоп-лосса в пунктах для лонга/шорта.
TakeProfitBuyPips / TakeProfitSellPips Размер тейк-профита в пунктах для лонга/шорта.
TrailingStopBuyPips / TrailingStopSellPips Дистанция трейлинг-стопа в пунктах для лонга/шорта.
TrailingStepPips Минимальный шаг изменения трейлинг-стопа (в пунктах).
CandleType Тип свечей, используемых в расчётах (по умолчанию — часовые).

Управление позициями

  • При открытии позиции фиксируется цена входа, рассчитываются уровни стоп-лосса и тейк-профита в абсолютных ценах, трейлинг-стоп сбрасывается.
  • Для длинных позиций стоп подтягивается выше, когда прибыль превышает сумму TrailingStopBuyPips + TrailingStepPips. Закрытие происходит по стопу или тейк-профиту — что наступит раньше.
  • Для коротких позиций стоп опускается ниже при аналогичном условии, закрытие выполняется симметрично.

Дополнительные замечания

  • Размер пункта определяется по шагу цены инструмента; для инструментов с тремя или пятью знаками после запятой пункт умножается на десять, как в оригинальном советнике.
  • Стратегия работает только с одной позицией одновременно: новый вход возможен после полного закрытия предыдущего.
  • Метод StartProtection() уже активирован, что защищает от «зависших» позиций после перезапуска.
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>
/// Channels envelope crossover strategy converted from the MetaTrader Channels expert advisor.
/// The strategy monitors EMA based envelopes on hourly candles and trades breakouts of the fast EMA through the bands.
/// </summary>
public class ChannelsEnvelopeCrossStrategy : Strategy
{
	private readonly StrategyParam<decimal> _envelope003;
	private readonly StrategyParam<decimal> _envelope007;
	private readonly StrategyParam<decimal> _envelope010;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _useTradeHours;
	private readonly StrategyParam<int> _fromHour;
	private readonly StrategyParam<int> _toHour;
	private readonly StrategyParam<int> _stopLossBuyPips;
	private readonly StrategyParam<int> _stopLossSellPips;
	private readonly StrategyParam<int> _takeProfitBuyPips;
	private readonly StrategyParam<int> _takeProfitSellPips;
	private readonly StrategyParam<int> _trailingStopBuyPips;
	private readonly StrategyParam<int> _trailingStopSellPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _emaFastClose;
	private ExponentialMovingAverage _emaFastOpen;
	private ExponentialMovingAverage _emaSlow;

	private bool _hasPreviousValues;
	private decimal _prevFastClose;
	private decimal _prevFastOpen;
	private decimal _prevSlow;
	private decimal _prevEnvLower03;
	private decimal _prevEnvUpper03;
	private decimal _prevEnvLower07;
	private decimal _prevEnvUpper07;
	private decimal _prevEnvLower10;
	private decimal _prevEnvUpper10;

	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Order volume used for market entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Enable trading only within the configured time window.
	/// </summary>
	public bool UseTradeHours
	{
		get => _useTradeHours.Value;
		set => _useTradeHours.Value = value;
	}

	/// <summary>
	/// Start hour of the trading window (inclusive).
	/// </summary>
	public int FromHour
	{
		get => _fromHour.Value;
		set => _fromHour.Value = value;
	}

	/// <summary>
	/// End hour of the trading window (inclusive).
	/// </summary>
	public int ToHour
	{
		get => _toHour.Value;
		set => _toHour.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for long positions expressed in pips.
	/// </summary>
	public int StopLossBuyPips
	{
		get => _stopLossBuyPips.Value;
		set => _stopLossBuyPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for short positions expressed in pips.
	/// </summary>
	public int StopLossSellPips
	{
		get => _stopLossSellPips.Value;
		set => _stopLossSellPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance for long positions expressed in pips.
	/// </summary>
	public int TakeProfitBuyPips
	{
		get => _takeProfitBuyPips.Value;
		set => _takeProfitBuyPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance for short positions expressed in pips.
	/// </summary>
	public int TakeProfitSellPips
	{
		get => _takeProfitSellPips.Value;
		set => _takeProfitSellPips.Value = value;
	}

	/// <summary>
	/// Trailing-stop size for long positions expressed in pips.
	/// </summary>
	public int TrailingStopBuyPips
	{
		get => _trailingStopBuyPips.Value;
		set => _trailingStopBuyPips.Value = value;
	}

	/// <summary>
	/// Trailing-stop size for short positions expressed in pips.
	/// </summary>
	public int TrailingStopSellPips
	{
		get => _trailingStopSellPips.Value;
		set => _trailingStopSellPips.Value = value;
	}

	/// <summary>
	/// Minimum increment for trailing adjustments expressed in pips.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Percentage width for the 0.3% envelope band.
	/// </summary>
	public decimal Envelope003
	{
		get => _envelope003.Value;
		set => _envelope003.Value = value;
	}

	/// <summary>
	/// Percentage width for the 0.7% envelope band.
	/// </summary>
	public decimal Envelope007
	{
		get => _envelope007.Value;
		set => _envelope007.Value = value;
	}

	/// <summary>
	/// Percentage width for the 1.0% envelope band.
	/// </summary>
	public decimal Envelope010
	{
		get => _envelope010.Value;
		set => _envelope010.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="ChannelsEnvelopeCrossStrategy"/>.
	/// </summary>
	public ChannelsEnvelopeCrossStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Order volume in lots", "Trading");

		_useTradeHours = Param(nameof(UseTradeHours), false)
		.SetDisplay("Use Trade Hours", "Restrict trading to specified hours", "Trading");

		_fromHour = Param(nameof(FromHour), 0)
		.SetDisplay("From Hour", "Start hour for trading window", "Trading");

		_toHour = Param(nameof(ToHour), 23)
		.SetDisplay("To Hour", "End hour for trading window", "Trading");

		_stopLossBuyPips = Param(nameof(StopLossBuyPips), 0)
		.SetDisplay("SL BUY (pips)", "Stop loss distance for long positions", "Risk");

		_stopLossSellPips = Param(nameof(StopLossSellPips), 0)
		.SetDisplay("SL SELL (pips)", "Stop loss distance for short positions", "Risk");

		_takeProfitBuyPips = Param(nameof(TakeProfitBuyPips), 0)
		.SetDisplay("TP BUY (pips)", "Take profit distance for long positions", "Risk");

		_takeProfitSellPips = Param(nameof(TakeProfitSellPips), 0)
		.SetDisplay("TP SELL (pips)", "Take profit distance for short positions", "Risk");

		_trailingStopBuyPips = Param(nameof(TrailingStopBuyPips), 30)
		.SetDisplay("Trail BUY (pips)", "Trailing stop for long positions", "Risk");

		_trailingStopSellPips = Param(nameof(TrailingStopSellPips), 30)
		.SetDisplay("Trail SELL (pips)", "Trailing stop for short positions", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 1)
		.SetDisplay("Trailing Step (pips)", "Minimum increment for trailing stop", "Risk");

		_envelope003 = Param(nameof(Envelope003), 0.3m / 100m)
			.SetGreaterThanZero()
			.SetDisplay("Envelope 0.3%", "Width of the 0.3% envelope", "Indicators");

		_envelope007 = Param(nameof(Envelope007), 0.7m / 100m)
			.SetGreaterThanZero()
			.SetDisplay("Envelope 0.7%", "Width of the 0.7% envelope", "Indicators");

		_envelope010 = Param(nameof(Envelope010), 1.0m / 100m)
			.SetGreaterThanZero()
			.SetDisplay("Envelope 1.0%", "Width of the 1.0% envelope", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for calculations", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
	return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_hasPreviousValues = false;
		_prevFastClose = 0m;
		_prevFastOpen = 0m;
		_prevSlow = 0m;
		_prevEnvLower03 = 0m;
		_prevEnvUpper03 = 0m;
		_prevEnvLower07 = 0m;
		_prevEnvUpper07 = 0m;
		_prevEnvLower10 = 0m;
		_prevEnvUpper10 = 0m;

		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;

		_emaFastClose?.Reset();
		_emaFastOpen?.Reset();
		_emaSlow?.Reset();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_emaFastClose = new ExponentialMovingAverage { Length = 2 };
		_emaFastOpen = new ExponentialMovingAverage { Length = 2 };
		_emaSlow = new ExponentialMovingAverage { Length = 220 };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(ProcessCandle)
		.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
	if (UseTradeHours && !IsWithinTradeHours(candle.OpenTime))
	return;

	if (candle.State != CandleStates.Finished)
	return;

	var fastCloseValue = _emaFastClose.Process(new DecimalIndicatorValue(_emaFastClose, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
	var fastOpenValue = _emaFastOpen.Process(new DecimalIndicatorValue(_emaFastOpen, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
	var slowValue = _emaSlow.Process(new DecimalIndicatorValue(_emaSlow, candle.ClosePrice, candle.OpenTime) { IsFinal = true });

	var fastClose = fastCloseValue.GetValue<decimal>();
	var fastOpen = fastOpenValue.GetValue<decimal>();
	var slow = slowValue.GetValue<decimal>();

	var envLower03 = slow * (1m - Envelope003);
	var envUpper03 = slow * (1m + Envelope003);
	var envLower07 = slow * (1m - Envelope007);
	var envUpper07 = slow * (1m + Envelope007);
	var envLower10 = slow * (1m - Envelope010);
	var envUpper10 = slow * (1m + Envelope010);

	if (!_emaSlow.IsFormed || !_emaFastClose.IsFormed || !_emaFastOpen.IsFormed)
	{
	UpdatePreviousValues(fastClose, fastOpen, slow, envLower03, envUpper03, envLower07, envUpper07, envLower10, envUpper10);
	return;
	}

	if (!_hasPreviousValues)
	{
	UpdatePreviousValues(fastClose, fastOpen, slow, envLower03, envUpper03, envLower07, envUpper07, envLower10, envUpper10);
	_hasPreviousValues = true;
	return;
	}

	var buySignal =
	(fastClose > envLower10 && _prevFastClose <= _prevEnvLower10) ||
	(fastClose > envLower07 && _prevFastClose <= _prevEnvLower07) ||
	(fastClose < envLower03 && _prevFastClose < _prevEnvLower03) ||
	(fastClose > slow && _prevFastClose <= _prevSlow) ||
	(fastClose > envUpper03 && _prevFastClose <= _prevEnvUpper03) ||
	(fastClose > envUpper07 && _prevFastClose <= _prevEnvUpper07);

	var sellSignal =
	(fastOpen < envUpper10 && _prevFastOpen >= _prevEnvUpper10) ||
	(fastOpen < envUpper07 && _prevFastOpen >= _prevEnvUpper07) ||
	(fastOpen < envUpper03 && _prevFastOpen >= _prevEnvUpper03) ||
	(fastOpen < slow && _prevFastOpen >= _prevSlow) ||
	(fastOpen < envLower03 && _prevFastOpen >= _prevEnvLower03) ||
	(fastOpen < envLower07 && _prevFastOpen >= _prevEnvLower07);

	if (Position > 0)
	{
	ManageLongPosition(candle);
	}
	else if (Position < 0)
	{
	ManageShortPosition(candle);
	}

	if (Position == 0)
	{
	if (buySignal)
	{
	BuyMarket(OrderVolume);
	SetEntryState(true, candle.ClosePrice);
	}
	else if (sellSignal)
	{
	SellMarket(OrderVolume);
	SetEntryState(false, candle.ClosePrice);
	}
	}

	UpdatePreviousValues(fastClose, fastOpen, slow, envLower03, envUpper03, envLower07, envUpper07, envLower10, envUpper10);
	}

	private void ManageLongPosition(ICandleMessage candle)
	{
	if (_entryPrice is null)
	return;

	var pip = GetPipSize();
	var trailingDistance = TrailingStopBuyPips * pip;
	var trailingStep = TrailingStepPips * pip;

	var profit = candle.ClosePrice - _entryPrice.Value;

	if (TrailingStopBuyPips > 0 && profit > trailingDistance + trailingStep)
	{
	var threshold = candle.ClosePrice - (trailingDistance + trailingStep);
	if (!_stopLossPrice.HasValue || _stopLossPrice.Value < threshold)
	_stopLossPrice = candle.ClosePrice - trailingDistance;
	}

	var exitVolume = Position;

	if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
	{
	SellMarket(exitVolume);
	ResetPositionState();
	return;
	}

	if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
	{
	SellMarket(exitVolume);
	ResetPositionState();
	}
	}

	private void ManageShortPosition(ICandleMessage candle)
	{
	if (_entryPrice is null)
	return;

	var pip = GetPipSize();
	var trailingDistance = TrailingStopSellPips * pip;
	var trailingStep = TrailingStepPips * pip;

	var profit = _entryPrice.Value - candle.ClosePrice;

	if (TrailingStopSellPips > 0 && profit > trailingDistance + trailingStep)
	{
	var threshold = candle.ClosePrice + (trailingDistance + trailingStep);
	if (!_stopLossPrice.HasValue || _stopLossPrice.Value > threshold)
	_stopLossPrice = candle.ClosePrice + trailingDistance;
	}

	var exitVolume = -Position;

	if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
	{
	BuyMarket(exitVolume);
	ResetPositionState();
	return;
	}

	if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
	{
	BuyMarket(exitVolume);
	ResetPositionState();
	}
	}

	private void SetEntryState(bool isLong, decimal entryPrice)
	{
	_entryPrice = entryPrice;

	var pip = GetPipSize();

	_stopLossPrice = isLong && StopLossBuyPips > 0
	? entryPrice - StopLossBuyPips * pip
	: !isLong && StopLossSellPips > 0
	? entryPrice + StopLossSellPips * pip
	: null;

	_takeProfitPrice = isLong && TakeProfitBuyPips > 0
	? entryPrice + TakeProfitBuyPips * pip
	: !isLong && TakeProfitSellPips > 0
	? entryPrice - TakeProfitSellPips * pip
	: null;
	}

	private void ResetPositionState()
	{
	_entryPrice = null;
	_stopLossPrice = null;
	_takeProfitPrice = null;
	}

	private void UpdatePreviousValues(decimal fastClose, decimal fastOpen, decimal slow, decimal envLower03, decimal envUpper03, decimal envLower07, decimal envUpper07, decimal envLower10, decimal envUpper10)
	{
	_prevFastClose = fastClose;
	_prevFastOpen = fastOpen;
	_prevSlow = slow;
	_prevEnvLower03 = envLower03;
	_prevEnvUpper03 = envUpper03;
	_prevEnvLower07 = envLower07;
	_prevEnvUpper07 = envUpper07;
	_prevEnvLower10 = envLower10;
	_prevEnvUpper10 = envUpper10;
	}

	private bool IsWithinTradeHours(DateTimeOffset time)
	{
	var hour = time.Hour;

	if (FromHour == ToHour)
	return hour == FromHour;

	if (FromHour < ToHour)
	return hour >= FromHour && hour <= ToHour;

	return hour >= FromHour || hour <= ToHour;
	}

	private decimal GetPipSize()
	{
	var step = Security?.PriceStep ?? 0.0001m;

	if (Security?.Decimals is int decimals && (decimals == 3 || decimals == 5))
	return step * 10m;

	return step;
	}
}