GitHub で見る

Currencyprofits High-Low Channel Strategy

Overview

This strategy is a StockSharp port of the MetaTrader expert advisor Currencyprofits_01.1. It combines a fast/slow moving average trend filter with a breakout of the recent channel extreme. When the fast moving average is above the slow average, the strategy expects a bullish environment and waits for price to retest the lowest low of the previous channel window. Short trades are taken when the fast average is below the slow average and price retests the highest high of the channel.

The implementation works on any instrument that provides candle data. All calculations are performed on closed candles to ensure stability in both backtests and live trading.

Trading logic

  1. Subscribe to the configured candle type and compute two moving averages and a Donchian-style channel based on the previous ChannelLength candles (default 6 bars).
  2. Store the previous candle values from the indicators to mimic the original MQL logic that uses a one-bar shift.
  3. Long entry: when the previous fast MA is greater than the previous slow MA and the current candle low touches or breaks the previous channel low.
  4. Short entry: when the previous fast MA is less than the previous slow MA and the current candle high touches or breaks the previous channel high.
  5. Exit rules:
    • Close long positions if the next candle closes above the stored channel high or if the protective stop is hit.
    • Close short positions if the next candle closes below the stored channel low or if the protective stop is hit.
  6. Only one position is active at a time; the strategy ignores new signals while a trade is open.

Position sizing

  • RiskPercent defines the fraction of the portfolio value that can be risked per trade (default 0.14, i.e., 14%).
  • The stop-loss distance is derived from StopLossPoints multiplied by the security PriceStep (or points if no metadata is available).
  • Cash risk per contract is estimated with the exchange step value (StepPrice). If the security does not expose this information, the raw price distance is used instead.
  • The final order volume is aligned to the instrument trading constraints (VolumeStep, MinVolume, MaxVolume). If risk-based sizing cannot be calculated, the base Volume of the strategy is used.

Parameters

  • FastLength – length of the fast moving average used to detect the trend (default 32).
  • FastMaType – type of the fast moving average (Simple, Exponential, Smoothed, Weighted).
  • SlowLength – length of the slow moving average (default 86).
  • SlowMaType – type of the slow moving average.
  • PriceSource – candle price applied to both moving averages (default Close).
  • ChannelLength – number of previous candles that form the high/low channel (default 6).
  • StopLossPoints – stop distance expressed in instrument points before it is converted to a price (default 170).
  • RiskPercent – fraction of equity risked per trade (default 0.14 → 14%).
  • CandleType – timeframe of the candles used for all calculations (default 1 hour, can be changed to match the desired chart period).

Usage notes

  • Ensure Security.PriceStep, Security.StepPrice, and volume metadata are filled for accurate position sizing.
  • Set the strategy Volume to a sensible fallback value when risk-based sizing is disabled (e.g., RiskPercent = 0).
  • The logic trades on closed candles; live executions occur on the bar close that confirms the signal.
  • The stop-loss is managed internally; there is no separate take-profit, mirroring the source expert advisor.

Source

Converted from MQL/17641/Currencyprofits_01.1.mq5 with emphasis on readability and compatibility with the high-level StockSharp API.

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>
/// Currencyprofits strategy that trades trend pullbacks into the recent channel extremes.
/// </summary>
public class CurrencyprofitsHighLowChannelStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _channelLength;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<CandlePrices> _priceSource;
	private readonly StrategyParam<MovingAverageTypes> _fastMaType;
	private readonly StrategyParam<MovingAverageTypes> _slowMaType;
	private readonly StrategyParam<int> _signalCooldownBars;

	private decimal? _previousFast;
	private decimal? _previousSlow;
	private decimal? _previousHighest;
	private decimal? _previousLowest;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private int _processedCandles;
	private int _cooldownRemaining;

	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	public int ChannelLength
	{
		get => _channelLength.Value;
		set => _channelLength.Value = value;
	}

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

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

	public CandlePrices PriceSource
	{
		get => _priceSource.Value;
		set => _priceSource.Value = value;
	}

	public MovingAverageTypes FastMaType
	{
		get => _fastMaType.Value;
		set => _fastMaType.Value = value;
	}

	public MovingAverageTypes SlowMaType
	{
		get => _slowMaType.Value;
		set => _slowMaType.Value = value;
	}

	public int SignalCooldownBars
	{
		get => _signalCooldownBars.Value;
		set => _signalCooldownBars.Value = value;
	}

	private int RequiredBars => Math.Max(Math.Max(FastLength, SlowLength), ChannelLength) + 1;

	public CurrencyprofitsHighLowChannelStrategy()
	{
		_fastLength = Param(nameof(FastLength), 32)
			.SetDisplay("Fast MA Length", "Length of the fast moving average", "Indicators")
			
			.SetOptimize(10, 120, 2);

		_slowLength = Param(nameof(SlowLength), 86)
			.SetDisplay("Slow MA Length", "Length of the slow moving average", "Indicators")
			
			.SetOptimize(20, 200, 2);

		_channelLength = Param(nameof(ChannelLength), 12)
			.SetDisplay("Channel Lookback", "Number of previous candles for high/low channel", "Indicators")
			
			.SetOptimize(3, 20, 1);

		_stopLossPoints = Param(nameof(StopLossPoints), 170m)
			.SetDisplay("Stop Loss (points)", "Distance to stop loss expressed in price steps", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 0.14m)
			.SetDisplay("Risk Fraction", "Fraction of portfolio capital risked per trade", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "General");

		_priceSource = Param(nameof(PriceSource), CandlePrices.Close)
			.SetDisplay("MA Price Source", "Price source used by both moving averages", "Indicators");

		_fastMaType = Param(nameof(FastMaType), MovingAverageTypes.Simple)
			.SetDisplay("Fast MA Type", "Moving average algorithm for the fast line", "Indicators");

		_slowMaType = Param(nameof(SlowMaType), MovingAverageTypes.Simple)
			.SetDisplay("Slow MA Type", "Moving average algorithm for the slow line", "Indicators");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
			.SetNotNegative()
			.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before the next entry", "Risk");
	}

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

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

		_previousFast = null;
		_previousSlow = null;
		_previousHighest = null;
		_previousLowest = null;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_processedCandles = 0;
		_cooldownRemaining = 0;
	}

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

		var fastMa = CreateMovingAverage(FastMaType, FastLength, PriceSource);
		var slowMa = CreateMovingAverage(SlowMaType, SlowLength, PriceSource);
		var highest = new Highest { Length = ChannelLength };
		var lowest = new Lowest { Length = ChannelLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(fastMa, slowMa, highest, lowest, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow, decimal channelHigh, decimal channelLow)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		_processedCandles++;

		if (_processedCandles <= RequiredBars)
		{
			// Collect enough history before taking any decisions.
			_previousFast = fast;
			_previousSlow = slow;
			_previousHighest = channelHigh;
			_previousLowest = channelLow;
			return;
		}

		if (_previousFast is null || _previousSlow is null || _previousHighest is null || _previousLowest is null)
		{
			_previousFast = fast;
			_previousSlow = slow;
			_previousHighest = channelHigh;
			_previousLowest = channelLow;
			return;
		}

		if (Position > 0)
		{
			// Exit long trades when price breaks the opposite channel or the protective stop.
			var exitByChannel = candle.ClosePrice >= _previousHighest.Value;
			var exitByStop = _stopPrice > 0m && candle.LowPrice <= _stopPrice;

			if (exitByChannel || exitByStop)
			{
				SellMarket(Position);
				ResetTradeState();
				_cooldownRemaining = SignalCooldownBars;
			}
		}
		else if (Position < 0)
		{
			// Exit short trades when price hits the lower boundary or the stop.
			var exitByChannel = candle.ClosePrice <= _previousLowest.Value;
			var exitByStop = _stopPrice > 0m && candle.HighPrice >= _stopPrice;

			if (exitByChannel || exitByStop)
			{
				BuyMarket(-Position);
				ResetTradeState();
				_cooldownRemaining = SignalCooldownBars;
			}
		}
		else if (_cooldownRemaining == 0)
		{
			var stopDistance = GetStopDistance();

			if (stopDistance > 0m)
			{
				var bullishTrend = _previousFast.Value > _previousSlow.Value && fast > slow;
				var bearishTrend = _previousFast.Value < _previousSlow.Value && fast < slow;
				var bullishReversal = candle.LowPrice <= _previousLowest.Value && candle.ClosePrice > candle.OpenPrice && candle.ClosePrice > fast;
				var bearishReversal = candle.HighPrice >= _previousHighest.Value && candle.ClosePrice < candle.OpenPrice && candle.ClosePrice < fast;

				// Long entries require a bullish trend and a pullback to the recent low channel.
				if (bullishTrend && bullishReversal)
				{
					var volume = CalculatePositionSize(stopDistance);

					if (volume > 0m)
					{
						BuyMarket(volume);
						_entryPrice = candle.ClosePrice;
						_stopPrice = _entryPrice - stopDistance;
						_cooldownRemaining = SignalCooldownBars;
					}
				}
				// Short entries require a bearish trend and a retest of the recent high channel.
				else if (bearishTrend && bearishReversal)
				{
					var volume = CalculatePositionSize(stopDistance);

					if (volume > 0m)
					{
						SellMarket(volume);
						_entryPrice = candle.ClosePrice;
						_stopPrice = _entryPrice + stopDistance;
						_cooldownRemaining = SignalCooldownBars;
					}
				}
			}
		}

		_previousFast = fast;
		_previousSlow = slow;
		_previousHighest = channelHigh;
		_previousLowest = channelLow;
	}

	private decimal GetStopDistance()
	{
		if (StopLossPoints <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep > 0m)
			return StopLossPoints * priceStep;

		return StopLossPoints;
	}

	private decimal CalculatePositionSize(decimal stopDistance)
	{
		var defaultVolume = AdjustVolume(Volume);

		if (stopDistance <= 0m)
			return defaultVolume;

		var portfolioValue = Portfolio?.CurrentValue;

		if (portfolioValue is null || portfolioValue <= 0m || RiskPercent <= 0m)
			return defaultVolume;

		var riskCapital = portfolioValue.Value * RiskPercent;
		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;

		decimal riskPerContract;

		if (priceStep > 0m && stepPrice > 0m)
		{
			// Convert the stop distance into cash risk per contract using exchange specifications.
			riskPerContract = stopDistance / priceStep * stepPrice;
		}
		else
		{
			// Fallback when the security does not expose step metadata.
			riskPerContract = stopDistance;
		}

		if (riskPerContract <= 0m)
			return defaultVolume;

		var desiredVolume = riskCapital / riskPerContract;
		return AdjustVolume(desiredVolume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;

		if (step > 0m)
		{
			var steps = decimal.Floor(volume / step);
			volume = steps * step;
		}

		var minVolume = Security?.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var maxVolume = Security?.MaxVolume ?? 0m;
		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private void ResetTradeState()
	{
		// Clear cached execution details after a position has been closed.
		_entryPrice = 0m;
		_stopPrice = 0m;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type, int length, CandlePrices price)
	{
		return type switch
		{
			MovingAverageTypes.Simple => new SMA { Length = length },
			MovingAverageTypes.Exponential => new EMA { Length = length },
			MovingAverageTypes.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageTypes.Weighted => new WeightedMovingAverage { Length = length },
			_ => new SMA { Length = length },
		};
	}

	public enum MovingAverageTypes
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted,
	}

	public enum CandlePrices
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}
}