Ver en GitHub

Channels Envelope Cross Strategy

Overview

The Channels Envelope Cross strategy is a direct port of the MetaTrader "Channels" expert advisor. The system trades hourly candles and monitors a fast two-period exponential moving average (EMA) relative to three EMA-based envelopes (0.3%, 0.7% and 1.0% deviations) that are calculated from a slow 220-period EMA. Breakouts of the fast EMA through these envelopes generate directional entries, while an optional time filter restricts trading to specific hours.

Trading Logic

  1. Indicator stack
    • Fast EMA (length 2) calculated on candle close prices.
    • Fast EMA (length 2) calculated on candle open prices.
    • Slow EMA (length 220) calculated on candle close prices.
    • Three envelope levels derived from the slow EMA with 0.3%, 0.7% and 1.0% deviations.
  2. Long setup
    • Triggered when the fast close EMA crosses above either the 1.0% or 0.7% lower envelope, remains below the 0.3% lower envelope for two consecutive bars, crosses above the slow EMA, or breaks through the 0.3% or 0.7% upper envelope. Any of these conditions can fire a long entry when no position is open.
  3. Short setup
    • Triggered when the fast open EMA crosses below any of the upper envelopes, drops below the slow EMA, or pierces the lower envelopes from above. Any of these conditions can fire a short entry when no position is open.
  4. Risk management
    • Fixed stop-loss and take-profit levels (per side) are expressed in pips and converted to price distance by using the instrument tick size. If the inputs are set to zero, the respective level is not applied.
    • Independent trailing stops for long and short positions move the protective stop closer to market price when the profit exceeds the trailing distance plus a configurable step increment.
  5. Time filter
    • When enabled, the strategy only processes entries during the configured inclusive hour range. Positions are still managed when the filter is active.

Parameters

Parameter Description
OrderVolume Order size used for market entries (lots or contracts depending on the security).
UseTradeHours Enables the time filter for entries.
FromHour / ToHour Inclusive start and end hours for the trading window (supports overnight ranges).
StopLossBuyPips / StopLossSellPips Stop-loss distance for long/short trades expressed in pips.
TakeProfitBuyPips / TakeProfitSellPips Take-profit distance for long/short trades expressed in pips.
TrailingStopBuyPips / TrailingStopSellPips Trailing stop distance in pips for long/short trades.
TrailingStepPips Minimum increment (in pips) required to move a trailing stop.
CandleType Candle series used for calculations (default is 1-hour time frame).

Position Management

  • On entry the strategy stores the fill price, calculates stop-loss and take-profit targets in absolute price units, and resets trailing levels.
  • While a long position is open, the stop-loss is trailed upward whenever profit exceeds TrailingStopBuyPips + TrailingStepPips. The strategy exits at the stop-loss or take-profit whichever is hit first.
  • While a short position is open, the stop-loss is trailed downward using the short-side trailing parameters and exits are executed symmetrically.

Notes

  • The pip size is derived from the security tick size. For three- or five-decimal instruments the pip is multiplied by ten to emulate the MetaTrader logic.
  • The strategy works with a single position at a time. A new entry is only placed after the existing position has been closed.
  • Enable StartProtection in the base class to guard against unexpected open positions after restarts (already called in the implementation).
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;
	}
}