Ver en GitHub

Estrategia Exp BlauCMI

Descripción General

La estrategia recrea el asesor experto de MetaTrader 5 Exp_BlauCMI usando la API de alto nivel de StockSharp. Calcula el Blau Candle Momentum Index (CMI), una ratio de momentum con triple suavizado, en una serie de velas configurable y reacciona a los giros del oscilador. Las operaciones largas se abren cuando el indicador gira hacia arriba después de un movimiento descendente, las cortas cuando el indicador gira hacia abajo. El módulo mantiene la implementación totalmente dirigida por eventos: las órdenes se envían solo después de que se cierran las velas.

Lógica del indicador

  1. Dos fuentes de precio se seleccionan mediante Momentum Price y Reference Price. El momentum bruto es la diferencia entre el valor actual del primer precio y el valor retrasado del segundo precio. El retraso se controla mediante Momentum Depth.
  2. Tanto el momentum como su valor absoluto pasan por tres medias móviles consecutivas (First/Second/Third Smoothing). Se usa el mismo método de promediado para cada etapa y se puede elegir entre medias móviles simples, exponenciales, suavizadas (RMA) y ponderadas linealmente.
  3. El Blau CMI se calcula como 100 * smoothedMomentum / smoothedAbsMomentum. El indicador empieza a producir señales de trading una vez que la tercera etapa de suavizado ha acumulado suficientes barras.
  4. El parámetro Signal Shift determina cuántas velas cerradas hacia atrás inspecciona la estrategia antes de evaluar las reversiones (un valor de 1 reproduce el EA original y usa la última barra cerrada).

Reglas de trading

  • Entrada larga – permitida cuando Allow Long Entry está habilitado y se observa la secuencia de indicador Value[Signal Shift - 1] < Value[Signal Shift - 2] seguida de Value[Signal Shift] > Value[Signal Shift - 1], lo que significa que el oscilador acaba de girar hacia arriba. Las posiciones cortas existentes se cierran primero si Allow Short Exit está habilitado.
  • Entrada corta – permitida cuando Allow Short Entry está habilitado y el indicador gira hacia abajo (Value[Signal Shift - 1] > Value[Signal Shift - 2] y Value[Signal Shift] < Value[Signal Shift - 1]). Las posiciones largas existentes se cierran de antemano si Allow Long Exit está habilitado.
  • Salida larga – cuando hay una posición larga y se activa la condición de entrada corta, la posición se cierra si Allow Long Exit es verdadero.
  • Salida corta – cuando hay una posición corta y se activa la condición de entrada larga, la posición se cierra si Allow Short Exit es verdadero.
  • Todas las operaciones se ejecutan con órdenes de mercado usando el volumen especificado en Order Volume. Los brackets de stop-loss y take-profit protectores se adjuntan automáticamente mediante StartProtection y permanecen activos mientras la posición esté abierta.

Parámetros

  • Candle Type – tipo de dato (marco temporal u otra descripción de velas) utilizado para el cálculo del indicador y las decisiones de trading. El valor predeterminado son velas de 4 horas.
  • Smoothing Method – algoritmo de promediado compartido por las tres etapas de suavizado (Simple, Exponencial, Suavizado, Ponderado Lineal).
  • Momentum Depth – número de barras entre los dos puntos de precio que forman el momentum bruto.
  • First/Second/Third Smoothing – longitudes de las tres etapas de promediado aplicadas tanto al momentum como a su valor absoluto.
  • Signal Shift – número de velas ya cerradas a mirar hacia atrás al evaluar patrones de reversión (valor mínimo es 1).
  • Momentum Price – precio aplicado usado para el lado no retrasado del cálculo del momentum.
  • Reference Price – precio aplicado usado para el lado de comparación retrasado.
  • Allow Long Entry, Allow Short Entry – interruptores para permitir abrir operaciones en cada dirección.
  • Allow Long Exit, Allow Short Exit – interruptores que controlan si las señales opuestas cierran las respectivas posiciones.
  • Stop-Loss Points, Take-Profit Points – límites de riesgo medidos en pasos de precio (Security.PriceStep). Cuando se establecen en cero, el bracket correspondiente está deshabilitado.
  • Order Volume – cantidad absoluta utilizada al enviar órdenes de mercado. La estrategia también asigna este valor a la propiedad base Strategy.Volume.

Notas adicionales

  • Los métodos de suavizado soportados corresponden a indicadores de StockSharp: Media Móvil Simple, Media Móvil Exponencial, Media Móvil Suavizada (RMA) y Media Móvil Ponderada.
  • La constante de precio Demark replica la implementación de MT5 promediando los extremos de precio y el cierre de la vela antes de ajustar las distancias al máximo/mínimo.
  • Dado que los cálculos usan solo velas terminadas, la estrategia reacciona una vez por barra, coincidiendo con el comportamiento original del EA que verificaba barras nuevas mediante IsNewBar.
  • Stop-Loss Points y Take-Profit Points se interpretan como múltiplos del paso de precio del instrumento para mantener consistencia con las entradas basadas en puntos de la estrategia MQL5 original.
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the Exp_BlauCMI MetaTrader strategy using the Blau Candle Momentum Index.
/// </summary>
public class ExpBlauCmiStrategy : Strategy
{
	/// <summary>
	/// Price sources supported by the strategy.
	/// </summary>
	public enum AppliedPrices
	{
		Close = 1,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Simple,
		Quarter,
		TrendFollow0,
		TrendFollow1,
		Demark
	}

	/// <summary>
	/// Smoothing modes used in the multi-stage averages.
	/// </summary>
	public enum SmoothingMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<SmoothingMethods> _smoothingMethod;
	private readonly StrategyParam<int> _momentumLength;
	private readonly StrategyParam<int> _firstSmoothingLength;
	private readonly StrategyParam<int> _secondSmoothingLength;
	private readonly StrategyParam<int> _thirdSmoothingLength;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<AppliedPrices> _priceForClose;
	private readonly StrategyParam<AppliedPrices> _priceForOpen;
	private readonly StrategyParam<bool> _allowLongEntry;
	private readonly StrategyParam<bool> _allowShortEntry;
	private readonly StrategyParam<bool> _allowLongExit;
	private readonly StrategyParam<bool> _allowShortExit;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _orderVolume;

	private DecimalLengthIndicator _momentumStage1 = null!;
	private DecimalLengthIndicator _momentumStage2 = null!;
	private DecimalLengthIndicator _momentumStage3 = null!;
	private DecimalLengthIndicator _absStage1 = null!;
	private DecimalLengthIndicator _absStage2 = null!;
	private DecimalLengthIndicator _absStage3 = null!;

	private readonly List<decimal> _priceBuffer = new();
	private readonly List<decimal> _indicatorHistory = new();

	private decimal _priceStep;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;

	/// <summary>
	/// Initializes a new instance of the <see cref="ExpBlauCmiStrategy"/> class.
	/// </summary>
	public ExpBlauCmiStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for BlauCMI calculations", "General");

		_smoothingMethod = Param(nameof(MomentumSmoothing), SmoothingMethods.Exponential)
			.SetDisplay("Smoothing Method", "Averaging mode for the BlauCMI stages", "Indicator");

		_momentumLength = Param(nameof(MomentumLength), 1)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Depth", "Bars between compared prices", "Indicator");

		_firstSmoothingLength = Param(nameof(FirstSmoothingLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("First Smoothing", "Length of the first BlauCMI smoothing", "Indicator");

		_secondSmoothingLength = Param(nameof(SecondSmoothingLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Second Smoothing", "Length of the second BlauCMI smoothing", "Indicator");

		_thirdSmoothingLength = Param(nameof(ThirdSmoothingLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Third Smoothing", "Length of the third BlauCMI smoothing", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Shift", "Number of closed bars used for signals", "Trading");

		_priceForClose = Param(nameof(PriceForClose), AppliedPrices.Close)
			.SetDisplay("Momentum Price", "Price type for the leading leg", "Indicator");

		_priceForOpen = Param(nameof(PriceForOpen), AppliedPrices.Open)
			.SetDisplay("Reference Price", "Price type compared against the delayed bar", "Indicator");

		_allowLongEntry = Param(nameof(AllowLongEntry), true)
			.SetDisplay("Allow Long Entry", "Enable opening long trades", "Trading");

		_allowShortEntry = Param(nameof(AllowShortEntry), true)
			.SetDisplay("Allow Short Entry", "Enable opening short trades", "Trading");

		_allowLongExit = Param(nameof(AllowLongExit), true)
			.SetDisplay("Allow Long Exit", "Enable closing long trades on opposite signals", "Trading");

		_allowShortExit = Param(nameof(AllowShortExit), true)
			.SetDisplay("Allow Short Exit", "Enable closing short trades on opposite signals", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetRange(0, 100000)
			.SetDisplay("Stop-Loss Points", "Distance to stop-loss in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
			.SetRange(0, 100000)
			.SetDisplay("Take-Profit Points", "Distance to take-profit in price steps", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Contract volume used for entries", "Trading");
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Averaging method for momentum smoothing stages.
	/// </summary>
	public SmoothingMethods MomentumSmoothing
	{
		get => _smoothingMethod.Value;
		set => _smoothingMethod.Value = value;
	}

	/// <summary>
	/// Bars between the compared prices when computing raw momentum.
	/// </summary>
	public int MomentumLength
	{
		get => _momentumLength.Value;
		set => _momentumLength.Value = value;
	}

	/// <summary>
	/// Length of the first momentum smoothing stage.
	/// </summary>
	public int FirstSmoothingLength
	{
		get => _firstSmoothingLength.Value;
		set => _firstSmoothingLength.Value = value;
	}

	/// <summary>
	/// Length of the second momentum smoothing stage.
	/// </summary>
	public int SecondSmoothingLength
	{
		get => _secondSmoothingLength.Value;
		set => _secondSmoothingLength.Value = value;
	}

	/// <summary>
	/// Length of the third momentum smoothing stage.
	/// </summary>
	public int ThirdSmoothingLength
	{
		get => _thirdSmoothingLength.Value;
		set => _thirdSmoothingLength.Value = value;
	}

	/// <summary>
	/// Index of the closed bar that produces trading signals.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Applied price for the front leg of momentum.
	/// </summary>
	public AppliedPrices PriceForClose
	{
		get => _priceForClose.Value;
		set => _priceForClose.Value = value;
	}

	/// <summary>
	/// Applied price for the delayed leg of momentum.
	/// </summary>
	public AppliedPrices PriceForOpen
	{
		get => _priceForOpen.Value;
		set => _priceForOpen.Value = value;
	}

	/// <summary>
	/// Allow opening long positions.
	/// </summary>
	public bool AllowLongEntry
	{
		get => _allowLongEntry.Value;
		set => _allowLongEntry.Value = value;
	}

	/// <summary>
	/// Allow opening short positions.
	/// </summary>
	public bool AllowShortEntry
	{
		get => _allowShortEntry.Value;
		set => _allowShortEntry.Value = value;
	}

	/// <summary>
	/// Allow closing long positions when an opposite signal appears.
	/// </summary>
	public bool AllowLongExit
	{
		get => _allowLongExit.Value;
		set => _allowLongExit.Value = value;
	}

	/// <summary>
	/// Allow closing short positions when an opposite signal appears.
	/// </summary>
	public bool AllowShortExit
	{
		get => _allowShortExit.Value;
		set => _allowShortExit.Value = value;
	}

	/// <summary>
	/// Stop-loss distance measured in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance measured in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Order volume used for market entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_priceBuffer.Clear();
		_indicatorHistory.Clear();
		_priceStep = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
	}

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

		_priceStep = Security?.PriceStep ?? 1m;
		_stopLossDistance = StopLossPoints > 0 ? StopLossPoints * _priceStep : 0m;
		_takeProfitDistance = TakeProfitPoints > 0 ? TakeProfitPoints * _priceStep : 0m;

		StartProtection(
			TakeProfitPoints > 0 ? new Unit(_takeProfitDistance, UnitTypes.Absolute) : null,
			StopLossPoints > 0 ? new Unit(_stopLossDistance, UnitTypes.Absolute) : null);

		Volume = Math.Abs(OrderVolume);

		_momentumStage1 = CreateMovingAverage(MomentumSmoothing, FirstSmoothingLength);
		_absStage1 = CreateMovingAverage(MomentumSmoothing, FirstSmoothingLength);
		_momentumStage2 = CreateMovingAverage(MomentumSmoothing, SecondSmoothingLength);
		_absStage2 = CreateMovingAverage(MomentumSmoothing, SecondSmoothingLength);
		_momentumStage3 = CreateMovingAverage(MomentumSmoothing, ThirdSmoothingLength);
		_absStage3 = CreateMovingAverage(MomentumSmoothing, ThirdSmoothingLength);

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

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

	private DecimalLengthIndicator CreateMovingAverage(SmoothingMethods method, int length)
	{
		var normalized = Math.Max(1, length);

		return method switch
		{
			SmoothingMethods.Simple => new SMA { Length = normalized },
			SmoothingMethods.Smoothed => new SmoothedMovingAverage { Length = normalized },
			SmoothingMethods.LinearWeighted => new WeightedMovingAverage { Length = normalized },
			_ => new EMA { Length = normalized }
		};
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var frontPrice = GetAppliedPrice(candle, PriceForClose);
		var referencePrice = GetAppliedPrice(candle, PriceForOpen);

		var momentumDepth = Math.Max(1, MomentumLength);
		_priceBuffer.Add(referencePrice);
		while (_priceBuffer.Count > momentumDepth)
			try { _priceBuffer.RemoveAt(0); } catch { break; }

		if (_priceBuffer.Count < momentumDepth)
			return;

		var delayedPrice = _priceBuffer[0];
		var momentum = frontPrice - delayedPrice;
		var absMomentum = Math.Abs(momentum);
		var time = candle.ServerTime;

		var stage1 = _momentumStage1.Process(new DecimalIndicatorValue(_momentumStage1, momentum, time) { IsFinal = true }).ToDecimal();
		var absStage1 = _absStage1.Process(new DecimalIndicatorValue(_absStage1, absMomentum, time) { IsFinal = true }).ToDecimal();

		var stage2 = _momentumStage2.Process(new DecimalIndicatorValue(_momentumStage2, stage1, time) { IsFinal = true }).ToDecimal();
		var absStage2 = _absStage2.Process(new DecimalIndicatorValue(_absStage2, absStage1, time) { IsFinal = true }).ToDecimal();

		var stage3Value = _momentumStage3.Process(new DecimalIndicatorValue(_momentumStage3, stage2, time) { IsFinal = true });
		var absStage3Value = _absStage3.Process(new DecimalIndicatorValue(_absStage3, absStage2, time) { IsFinal = true });

		if (!stage3Value.IsFormed || !absStage3Value.IsFormed)
			return;

		var denominator = absStage3Value.ToDecimal();
		if (denominator == 0m)
			return;

		var cmi = 100m * stage3Value.ToDecimal() / denominator;

		_indicatorHistory.Add(cmi);
		var required = SignalBar + 3;
		if (_indicatorHistory.Count > required)
			_indicatorHistory.RemoveRange(0, _indicatorHistory.Count - required);

		var index = _indicatorHistory.Count - 1 - SignalBar;
		if (index < 2)
			return;

		var value0 = _indicatorHistory[index];
		var value1 = _indicatorHistory[index - 1];
		var value2 = _indicatorHistory[index - 2];

		var buySignal = value1 < value2 && value0 > value1;
		var sellSignal = value1 > value2 && value0 < value1;


		if (Position > 0 && AllowLongExit && sellSignal)
		{
			SellMarket();
		}

		if (Position < 0 && AllowShortExit && buySignal)
		{
			BuyMarket();
		}

		if (Position != 0)
			return;

		if (buySignal && AllowLongEntry)
		{
			BuyMarket();
		}
		else if (sellSignal && AllowShortEntry)
		{
			SellMarket();
		}
	}

	private static decimal GetAppliedPrice(ICandleMessage candle, AppliedPrices price)
	{
		return price switch
		{
			AppliedPrices.Close => candle.ClosePrice,
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 3m,
			AppliedPrices.Weighted => (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			AppliedPrices.Simple => (candle.OpenPrice + candle.ClosePrice) / 2m,
			AppliedPrices.Quarter => (candle.OpenPrice + candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			AppliedPrices.TrendFollow0 => candle.ClosePrice > candle.OpenPrice
				? candle.HighPrice
				: candle.ClosePrice < candle.OpenPrice
					? candle.LowPrice
					: candle.ClosePrice,
			AppliedPrices.TrendFollow1 => candle.ClosePrice > candle.OpenPrice
				? (candle.HighPrice + candle.ClosePrice) / 2m
				: candle.ClosePrice < candle.OpenPrice
					? (candle.LowPrice + candle.ClosePrice) / 2m
					: candle.ClosePrice,
			AppliedPrices.Demark =>
				GetDemarkPrice(candle),
			_ => candle.ClosePrice
		};
	}

	private static decimal GetDemarkPrice(ICandleMessage candle)
	{
		var baseValue = candle.HighPrice + candle.LowPrice + candle.ClosePrice;

		if (candle.ClosePrice < candle.OpenPrice)
			baseValue = (baseValue + candle.LowPrice) / 2m;
		else if (candle.ClosePrice > candle.OpenPrice)
			baseValue = (baseValue + candle.HighPrice) / 2m;
		else
			baseValue = (baseValue + candle.ClosePrice) / 2m;

		return ((baseValue - candle.LowPrice) + (baseValue - candle.HighPrice)) / 2m;
	}
}