Ver en GitHub

Estrategia Currencyprofits de Canal Alto-Bajo

Descripción general

Esta estrategia es un port en StockSharp del asesor experto de MetaTrader Currencyprofits_01.1. Combina un filtro de tendencia de medias móviles rápida/lenta con un rompimiento del extremo del canal reciente. Cuando la media móvil rápida está por encima de la lenta, la estrategia espera un entorno alcista y espera que el precio reteste el mínimo más bajo de la ventana del canal anterior. Las operaciones cortas se realizan cuando la media rápida está por debajo de la lenta y el precio retesta el máximo más alto del canal.

La implementación funciona en cualquier instrumento que proporcione datos de velas. Todos los cálculos se realizan en velas cerradas para garantizar estabilidad tanto en backtests como en operativa en vivo.

Lógica de trading

  1. Suscribirse al tipo de vela configurado y calcular dos medias móviles y un canal de estilo Donchian basado en las anteriores ChannelLength velas (por defecto 6 barras).
  2. Almacenar los valores anteriores de las velas de los indicadores para imitar la lógica MQL original que usa un desplazamiento de una barra.
  3. Entrada larga: cuando la MA rápida anterior es mayor que la MA lenta anterior y el mínimo de la vela actual toca o rompe el mínimo del canal anterior.
  4. Entrada corta: cuando la MA rápida anterior es menor que la MA lenta anterior y el máximo de la vela actual toca o rompe el máximo del canal anterior.
  5. Reglas de salida:
    • Cerrar posiciones largas si la siguiente vela cierra por encima del máximo del canal almacenado o si se alcanza el stop protector.
    • Cerrar posiciones cortas si la siguiente vela cierra por debajo del mínimo del canal almacenado o si se activa el stop-loss.
  6. Solo una posición está activa a la vez; la estrategia ignora nuevas señales mientras una operación está abierta.

Dimensionamiento de posición

  • RiskPercent define la fracción del valor del portafolio que puede arriesgarse por operación (por defecto 0.14, es decir, 14%).
  • La distancia del stop-loss se deriva de StopLossPoints multiplicada por el PriceStep del instrumento (o puntos si no hay metadatos disponibles).
  • El riesgo en efectivo por contrato se estima con el valor de paso de intercambio (StepPrice). Si el instrumento no expone esta información, se usa la distancia de precio bruta.
  • El volumen final de la orden se alinea a las restricciones de trading del instrumento (VolumeStep, MinVolume, MaxVolume). Si el dimensionamiento basado en riesgo no puede calcularse, se usa el Volume base de la estrategia.

Parámetros

  • FastLength – longitud de la media móvil rápida usada para detectar la tendencia (por defecto 32).
  • FastMaType – tipo de la media móvil rápida (Simple, Exponential, Smoothed, Weighted).
  • SlowLength – longitud de la media móvil lenta (por defecto 86).
  • SlowMaType – tipo de la media móvil lenta.
  • PriceSource – precio de vela aplicado a ambas medias móviles (por defecto Close).
  • ChannelLength – número de velas anteriores que forman el canal alto/bajo (por defecto 6).
  • StopLossPoints – distancia del stop expresada en puntos del instrumento antes de convertirse a precio (por defecto 170).
  • RiskPercent – fracción del capital arriesgado por operación (por defecto 0.14 → 14%).
  • CandleType – marco temporal de las velas usadas para todos los cálculos (por defecto 1 hora, puede cambiarse para coincidir con el período del gráfico deseado).

Notas de uso

  • Asegurarse de que Security.PriceStep, Security.StepPrice y los metadatos de volumen estén completos para un dimensionamiento preciso de la posición.
  • Establecer el Volume de la estrategia a un valor de respaldo razonable cuando el dimensionamiento basado en riesgo esté deshabilitado (p. ej., RiskPercent = 0).
  • La lógica opera en velas cerradas; las ejecuciones en vivo ocurren al cierre de la barra que confirma la señal.
  • El stop-loss se gestiona internamente; no hay take-profit separado, lo que refleja el asesor experto fuente.

Fuente

Convertido de MQL/17641/Currencyprofits_01.1.mq5 con énfasis en legibilidad y compatibilidad con la API de alto nivel de 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
	}
}