Ver no GitHub

Percentage Crossover Channel System Strategy

This strategy is a direct port of the MetaTrader expert advisor Exp_PercentageCrossoverChannel_System. It tracks how price interacts with a custom "Percentage Crossover Channel" and reacts when candles move back inside the channel after previously breaking out. The code was rewritten with StockSharp high-level APIs and preserves the original signal flow.

Trading Logic

  1. Indicator construction

    • The Percentage Crossover Channel builds an adaptive midline that stays close to price but cannot drift away faster than a fixed percentage (Percent).
    • Upper and lower bands are derived from the midline using the same percentage distance.
    • Every completed candle is coloured according to its relationship with the channel from Shift bars ago:
      • Color 3 / 4: close above the upper band (bearish/bullish candle body respectively).
      • Color 0 / 1: close below the lower band (bearish/bullish body respectively).
      • Color 2: candle finished inside the channel.
  2. Entry and exit rules

    • Evaluate the last SignalBar candle and the one immediately preceding it (mirrors the MQL CopyBuffer call).
    • Bullish sequence (olderColor > 2): the market recently closed above the channel. If the most recent candle moved back inside (recentColor < 3) the strategy:
      • Closes any active short if SellPositionsClose is enabled.
      • Opens a long position when no trades are open and BuyPositionsOpen is enabled.
    • Bearish sequence (olderColor < 2): the market recently closed below the channel. If the latest candle returned inside (recentColor > 1) the strategy:
      • Closes any long if BuyPositionsClose is enabled.
      • Opens a short when no trades are active and SellPositionsOpen is enabled.
    • The logic therefore waits for a breakout followed by a re-entry into the channel before committing in the breakout direction.
  3. Risk management

    • Optional stop loss and take profit are expressed in price steps and evaluated on candle highs/lows.
    • If a protective order is triggered the strategy leaves the market and ignores fresh entries for the same bar, mimicking the MQL behaviour where broker-side stops close the trade first.

Parameters

Name Description
Percent Channel width in percent. Matches the MQL indicator input.
Shift Number of bars used to compare the breakout with historical bands.
SignalBar Offset (in bars) for signal evaluation. A value of 1 means "previous bar" like the original EA default.
BuyPositionsOpen / SellPositionsOpen Enable or disable opening trades in the corresponding direction.
BuyPositionsClose / SellPositionsClose Enable or disable force-closing opposite positions on a new signal.
StopLoss Stop loss distance expressed in multiples of Security.PriceStep. Set to zero to disable.
TakeProfit Take-profit distance in price steps. Set to zero to disable.
CandleType Timeframe for candle subscription. Defaults to four-hour bars to mirror PERIOD_H4.

Implementation Notes

  • The indicator logic is implemented inline because StockSharp does not provide a native Percentage Crossover Channel. The midline calculations, band derivation and colour assignments reproduce the MQL source algorithm step by step.
  • Position management follows the original helper functions (BuyPositionOpen, SellPositionOpen, etc.) by closing opposing trades before opening a new one and by skipping entries when an opposite position is still present.
  • Money management, deviation handling and margin-mode specific lot sizing from the original include file are not replicated. StockSharp users should configure the strategy volume via standard Strategy properties or the hosting environment.
  • Stop loss / take profit values are interpreted as price steps because MetaTrader inputs are specified in points. Ensure that the connected security exposes a valid PriceStep.

Usage Tips

  • Attach the strategy to an instrument with reliable four-hour data if you want behaviour identical to MetaTrader. Adjust CandleType to experiment with intraday operation.
  • Because the entry logic requires two completed candles with valid colour information, allow the strategy to warm up with at least Shift + SignalBar + 1 bars of history.
  • The channel is sensitive to the Percent input. Smaller values hug price tightly and increase trading frequency, whereas larger values focus on stronger breakouts.
  • When combining with portfolio-level risk controls, keep in mind that this implementation opens at most one position at a time and flips between long, flat or short states.
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>
/// Percentage Crossover Channel breakout system translated from MQL.
/// </summary>
public class PercentageCrossoverChannelSystemStrategy : Strategy
{
	private readonly StrategyParam<decimal> _percent;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _buyOpen;
	private readonly StrategyParam<bool> _sellOpen;
	private readonly StrategyParam<bool> _buyClose;
	private readonly StrategyParam<bool> _sellClose;
	private readonly StrategyParam<int> _stopLoss;
	private readonly StrategyParam<int> _takeProfit;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<int> _colorHistory = new();
	private readonly List<decimal> _upperHistory = new();
	private readonly List<decimal> _lowerHistory = new();

	private decimal _previousMiddle;
	private bool _hasMiddle;
	private decimal? _entryPrice;

	public decimal Percent
	{
		get => _percent.Value;
		set => _percent.Value = value;
	}

	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	public bool BuyPositionsOpen
	{
		get => _buyOpen.Value;
		set => _buyOpen.Value = value;
	}

	public bool SellPositionsOpen
	{
		get => _sellOpen.Value;
		set => _sellOpen.Value = value;
	}

	public bool BuyPositionsClose
	{
		get => _buyClose.Value;
		set => _buyClose.Value = value;
	}

	public bool SellPositionsClose
	{
		get => _sellClose.Value;
		set => _sellClose.Value = value;
	}

	public int StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	public int TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public PercentageCrossoverChannelSystemStrategy()
	{
		_percent = Param(nameof(Percent), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Channel Percent", "Percentage width of the channel", "Indicator");

		_shift = Param(nameof(Shift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Shift", "Number of bars used for crossover comparison", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Bar", "Bars back to evaluate indicator colors", "Trading Rules");

		_buyOpen = Param(nameof(BuyPositionsOpen), true)
			.SetDisplay("Enable Long Entries", "Allow long position openings", "Trading Rules");

		_sellOpen = Param(nameof(SellPositionsOpen), true)
			.SetDisplay("Enable Short Entries", "Allow short position openings", "Trading Rules");

		_buyClose = Param(nameof(BuyPositionsClose), true)
			.SetDisplay("Allow Long Exits", "Permit closing long trades on bearish signals", "Trading Rules");

		_sellClose = Param(nameof(SellPositionsClose), true)
			.SetDisplay("Allow Short Exits", "Permit closing short trades on bullish signals", "Trading Rules");

		_stopLoss = Param(nameof(StopLoss), 1000)
			.SetDisplay("Stop Loss (steps)", "Protective stop loss distance in price steps", "Risk Management");

		_takeProfit = Param(nameof(TakeProfit), 2000)
			.SetDisplay("Take Profit (steps)", "Target profit distance in price steps", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for analysis", "General");
	}

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

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

		_colorHistory.Clear();
		_upperHistory.Clear();
		_lowerHistory.Clear();
		_hasMiddle = false;
		_previousMiddle = 0m;
		_entryPrice = null;
	}

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore interim updates; we only react on closed candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Evaluate protective orders before generating new signals.
		var stopTriggered = HandleRisk(candle);

		// Mirror the MQL signal logic using cached indicator colors.
		if (_colorHistory.Count > SignalBar)
		{
			// Equivalent to CopyBuffer(..., SignalBar, 2, ...) from the EA.
			var recentIndex = _colorHistory.Count - SignalBar;
			var olderIndex = recentIndex - 1;

			if (olderIndex >= 0)
			{
				var recentColor = _colorHistory[recentIndex];
				var olderColor = _colorHistory[olderIndex];

				var shouldCloseShort = SellPositionsClose && olderColor > 2;
				var shouldCloseLong = BuyPositionsClose && olderColor < 2;
				var shouldOpenBuy = BuyPositionsOpen && olderColor > 2 && recentColor < 3;
				var shouldOpenSell = SellPositionsOpen && olderColor < 2 && recentColor > 1;

				// Close existing positions according to the original toggles.
				if (shouldCloseLong && Position > 0)
				{
					SellMarket();
					_entryPrice = null;
				}

				if (shouldCloseShort && Position < 0)
				{
					BuyMarket();
					_entryPrice = null;
				}

				// Enter only when we are flat to match the EA behaviour.
				if (!stopTriggered && Position == 0)
				{
					if (shouldOpenBuy)
					{
						BuyMarket();
						_entryPrice = candle.ClosePrice;
					}
					else if (shouldOpenSell)
					{
						SellMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
		}

		// Update indicator state after trading decisions are made.
		var color = CalculateColor(candle);

		_colorHistory.Add(color);
		TrimHistory();
	}

	private bool HandleRisk(ICandleMessage candle)
	{
		// Exit early if there is no stored entry price.
		if (_entryPrice is null)
			return false;

		// Price step is required to translate MQL points into absolute prices.
		if (Security?.PriceStep is not decimal step || step <= 0)
			return false;

		var triggered = false;

		if (Position > 0)
		{
			// Long position risk checks.
			if (StopLoss > 0)
			{
				var stopLevel = _entryPrice.Value - StopLoss * step;
				if (candle.LowPrice <= stopLevel)
				{
					SellMarket();
					_entryPrice = null;
					triggered = true;
				}
			}

			if (!triggered && TakeProfit > 0)
			{
				var takeLevel = _entryPrice.Value + TakeProfit * step;
				if (candle.HighPrice >= takeLevel)
				{
					SellMarket();
					_entryPrice = null;
					triggered = true;
				}
			}
		}
		else if (Position < 0)
		{
			// Short position risk checks.
			if (StopLoss > 0)
			{
				var stopLevel = _entryPrice.Value + StopLoss * step;
				if (candle.HighPrice >= stopLevel)
				{
					BuyMarket();
					_entryPrice = null;
					triggered = true;
				}
			}

			if (!triggered && TakeProfit > 0)
			{
				var takeLevel = _entryPrice.Value - TakeProfit * step;
				if (candle.LowPrice <= takeLevel)
				{
					BuyMarket();
					_entryPrice = null;
					triggered = true;
				}
			}
		}

		// Reset cached entry price once we are flat.
		if (Position == 0)
			_entryPrice = null;

		return triggered;
	}

	private int CalculateColor(ICandleMessage candle)
	{
		// Recreate the Percentage Crossover Channel midline and colour logic.
		var percentFactor = Percent / 100m;
		var plusVar = 1m + percentFactor;
		var minusVar = 1m - percentFactor;
		var close = candle.ClosePrice;

		// Initialise the midline on the very first candle.
		if (!_hasMiddle)
		{
			_previousMiddle = close;
			_hasMiddle = true;
		}

		var middle = _previousMiddle;
		var lowerCandidate = close * minusVar;
		var upperCandidate = close * plusVar;

		// Adjust the midline exactly as in the original indicator.
		if (lowerCandidate > _previousMiddle)
		{
			middle = lowerCandidate;
		}
		else if (upperCandidate < _previousMiddle)
		{
			middle = upperCandidate;
		}

		var upper = middle + middle * percentFactor;
		var lower = middle - middle * percentFactor;

		_previousMiddle = middle;

		var color = 2;

		// Determine candle colour relative to past channel values.
		if (_upperHistory.Count >= Shift)
		{
			var referenceIndex = _upperHistory.Count - Shift;
			var referenceUpper = _upperHistory[referenceIndex];
			var referenceLower = _lowerHistory[referenceIndex];

			if (close > referenceUpper)
			{
				color = candle.OpenPrice <= close ? 4 : 3;
			}
			else if (close < referenceLower)
			{
				color = candle.OpenPrice > close ? 0 : 1;
			}
		}

		// Persist channel history for future signal checks.
		_upperHistory.Add(upper);
		_lowerHistory.Add(lower);

		return color;
	}

	private void TrimHistory()
	{
		// Keep only as much history as needed for Shift and SignalBar lookbacks.
		var maxCapacity = Math.Max(Shift + SignalBar + 5, 16);
		if (_colorHistory.Count <= maxCapacity)
			return;

		var removeCount = _colorHistory.Count - maxCapacity;
		for (var i = 0; i < removeCount; i++)
		{
			try
			{
				_colorHistory.RemoveAt(0);
				_upperHistory.RemoveAt(0);
				_lowerHistory.RemoveAt(0);
			}
			catch { break; }
		}
	}
}