Ver no GitHub

Estratégia Exp BlauCMI

Visão Geral

A estratégia recria o assessor especialista do MetaTrader 5 Exp_BlauCMI usando a API de alto nível do StockSharp. Ela calcula o Blau Candle Momentum Index (CMI), uma razão de momentum com triplo suavizado, em uma série de candles configurável e reage às oscilações do oscilador. Operações compradas são abertas quando o indicador vira para cima após uma queda, as vendidas quando vira para baixo após uma alta. O módulo mantém a implementação totalmente orientada a eventos — as ordens são enviadas apenas após o fechamento dos candles.

Lógica do indicador

  1. Duas fontes de preço são selecionadas através de Momentum Price e Reference Price. O momentum bruto é a diferença entre o valor atual do primeiro preço e o valor atrasado do segundo preço. O atraso é controlado por Momentum Depth.
  2. Tanto o momentum quanto seu valor absoluto passam por três médias móveis consecutivas (First/Second/Third Smoothing). O mesmo método de média é usado para cada estágio e pode ser selecionado entre médias móveis simples, exponenciais, suavizadas (RMA) e ponderadas linearmente.
  3. O Blau CMI é calculado como 100 * smoothedMomentum / smoothedAbsMomentum. O indicador começa a produzir sinais de trading assim que o terceiro estágio de suavizado acumulou barras suficientes.
  4. O parâmetro Signal Shift determina quantos candles fechados para trás a estratégia inspeciona antes de avaliar reversões (um valor de 1 reproduz o EA original e usa a última barra fechada).

Regras de trading

  • Entrada comprada – permitida quando Allow Long Entry está habilitado e a sequência de indicador Value[Signal Shift - 1] < Value[Signal Shift - 2] seguida de Value[Signal Shift] > Value[Signal Shift - 1] é observada, significando que o oscilador acabou de virar para cima. Posições vendidas existentes são fechadas primeiro se Allow Short Exit estiver habilitado.
  • Entrada vendida – permitida quando Allow Short Entry está habilitado e o indicador vira para baixo (Value[Signal Shift - 1] > Value[Signal Shift - 2] e Value[Signal Shift] < Value[Signal Shift - 1]). Posições compradas existentes são fechadas de antemão se Allow Long Exit estiver habilitado.
  • Saída comprada – quando em uma posição comprada e a condição de entrada vendida dispara, a posição é fechada se Allow Long Exit for verdadeiro.
  • Saída vendida – quando em uma posição vendida e a condição de entrada comprada dispara, a posição é fechada se Allow Short Exit for verdadeiro.
  • Todas as negociações são executadas com ordens de mercado usando o volume especificado em Order Volume. Brackets de stop-loss e take-profit protetores são anexados automaticamente via StartProtection e permanecem ativos enquanto a posição estiver aberta.

Parâmetros

  • Candle Type – tipo de dado (período ou outra descrição de candles) usado para cálculo do indicador e decisões de trading. O padrão são candles de 4 horas.
  • Smoothing Method – algoritmo de média compartilhado pelos três estágios de suavizado (Simples, Exponencial, Suavizado, Ponderado Linear).
  • Momentum Depth – número de barras entre os dois pontos de preço que formam o momentum bruto.
  • First/Second/Third Smoothing – comprimentos dos três estágios de média aplicados tanto ao momentum quanto ao seu valor absoluto.
  • Signal Shift – número de candles já fechados a serem inspecionados ao avaliar padrões de reversão (valor mínimo é 1).
  • Momentum Price – preço aplicado usado para o lado não atrasado do cálculo do momentum.
  • Reference Price – preço aplicado usado para o lado de comparação atrasado.
  • Allow Long Entry, Allow Short Entry – interruptores para permitir a abertura de negociações em cada direção.
  • Allow Long Exit, Allow Short Exit – interruptores que controlam se os sinais opostos fecham as respectivas posições.
  • Stop-Loss Points, Take-Profit Points – limites de risco medidos em passos de preço (Security.PriceStep). Quando definidos como zero, o bracket correspondente é desabilitado.
  • Order Volume – quantidade absoluta usada ao enviar ordens de mercado. A estratégia também atribui esse valor à propriedade base Strategy.Volume.

Notas adicionais

  • Os métodos de suavizado suportados correspondem a indicadores do StockSharp: Média Móvel Simples, Média Móvel Exponencial, Média Móvel Suavizada (RMA) e Média Móvel Ponderada.
  • A constante de preço Demark replica a implementação do MT5 calculando a média dos extremos de preço e do fechamento do candle antes de ajustar as distâncias alta/baixa.
  • Como os cálculos usam apenas candles finalizados, a estratégia reage uma vez por barra, correspondendo ao comportamento original do EA que verificava novas barras via IsNewBar.
  • Stop-Loss Points e Take-Profit Points são interpretados como múltiplos do passo de preço do instrumento para manter consistência com as entradas baseadas em pontos da estratégia 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;
	}
}