Открыть на GitHub

Стратегия Currencyprofits High-Low Channel

Обзор

Данная стратегия — порт экспертного советника MetaTrader Currencyprofits_01.1 на платформу StockSharp. Комбинация быстрой и медленной скользящих средних используется для оценки направления тренда, а пробой экстремумов канала из последних баров служит точкой входа. Когда быстрая средняя находится выше медленной, предполагается восходящий тренд и стратегия ждёт отката к минимуму канала. При обратном соотношении средних ожидается нисходящее движение и отслеживается возврат к максимуму канала для открытия короткой позиции.

Все расчёты выполняются по закрытым свечам, поэтому алгоритм устойчив в тестах и на реальных данных.

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

  1. Подписка на выбранный тип свечей и расчёт двух скользящих средних вместе с каналом из последних ChannelLength баров (по умолчанию 6).
  2. Сохранение значений предыдущей свечи для индикаторов, что повторяет оригинальную MQL-реализацию с использованием сдвига на один бар.
  3. Вход в лонг: предыдущая быстрая средняя выше предыдущей медленной, текущий минимум свечи касается либо пробивает предыдущий минимум канала.
  4. Вход в шорт: предыдущая быстрая средняя ниже предыдущей медленной, текущий максимум свечи касается либо пробивает предыдущий максимум канала.
  5. Правила выхода:
    • Закрытие длинной позиции, если следующая свеча закрывается выше сохранённого максимума канала или достигается стоп-лосс.
    • Закрытие короткой позиции, если следующая свеча закрывается ниже сохранённого минимума канала или срабатывает стоп-лосс.
  6. Одновременно может быть открыта только одна позиция; пока сделка активна, новые сигналы игнорируются.

Управление риском

  • RiskPercent задаёт долю капитала, допускаемую к риску в одной сделке (по умолчанию 0.14, то есть 14%).
  • Расстояние стоп-лосса рассчитывается как StopLossPoints, умноженное на PriceStep инструмента (или используется значение в пунктах, если метаданные отсутствуют).
  • Денежный риск на контракт определяется через StepPrice. Если биржевые параметры недоступны, применяется чистая ценовая разница.
  • Итоговый объём приводится к требованиям инструмента (VolumeStep, MinVolume, MaxVolume). При невозможности вычислить объём по риску используется базовое значение Volume стратегии.

Параметры

  • FastLength — период быстрой скользящей средней (по умолчанию 32).
  • FastMaType — тип скользящей средней для быстрой линии (Simple, Exponential, Smoothed, Weighted).
  • SlowLength — период медленной скользящей средней (по умолчанию 86).
  • SlowMaType — тип скользящей средней для медленной линии.
  • PriceSource — цена свечи, применяемая при вычислении средних (по умолчанию Close).
  • ChannelLength — количество предыдущих свечей, формирующих канал максимумов/минимумов (по умолчанию 6).
  • StopLossPoints — расстояние до стоп-лосса в пунктах (по умолчанию 170).
  • RiskPercent — доля капитала под риском в сделке (по умолчанию 0.14 → 14%).
  • CandleType — тип свечей, используемых для расчётов (по умолчанию часовой таймфрейм, можно изменять под нужды стратегии).

Рекомендации по применению

  • Перед запуском заполните свойства инструмента PriceStep, StepPrice и параметры объёма для корректного расчёта размера позиции.
  • Установите разумное значение Volume, чтобы стратегия знала объём по умолчанию при отключённом управлении риском (например, RiskPercent = 0).
  • Сделки открываются по закрытию свечи, которая подтверждает сигнал.
  • Отдельного тейк-профита нет — логика выхода совпадает с оригинальным советником и использует стоп-лосс и обратный пробой канала.

Источник

Конвертировано из MQL/17641/Currencyprofits_01.1.mq5 с использованием высокоуровневого API 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;



/// <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
	}
}