GitHub で見る

Exp BlauCMI

Overview

The strategy recreates the MetaTrader 5 expert advisor Exp_BlauCMI using StockSharp's high level API. It computes the Blau Candle Momentum Index (CMI), a triple-smoothed momentum ratio, on a configurable candle series and reacts to swings in the oscillator. Long trades are opened when the indicator turns upward after a downswing, short trades are opened when the indicator turns downward after an upswing. The module keeps the implementation fully event driven – orders are sent only after candles are closed.

Indicator logic

  1. Two price sources are selected through Momentum Price and Reference Price. The raw momentum is the difference between the current value of the first price and the delayed value of the second price. The delay is controlled by Momentum Depth.
  2. Both the momentum and its absolute value are passed through three consecutive moving averages (First/Second/Third Smoothing). The same averaging method is used for every stage and can be selected among simple, exponential, smoothed (RMA) and linear weighted moving averages.
  3. The Blau CMI is calculated as 100 * smoothedMomentum / smoothedAbsMomentum. The indicator starts producing trading signals once the third smoothing stage has accumulated enough bars.
  4. The Signal Shift parameter determines how many closed candles back the strategy inspects before evaluating reversals (a value of 1 reproduces the original EA and uses the last closed bar).

Trading rules

  • Long entry – allowed when Allow Long Entry is enabled and the indicator sequence Value[Signal Shift - 1] < Value[Signal Shift - 2] followed by Value[Signal Shift] > Value[Signal Shift - 1] is observed, meaning the oscillator just turned upward. Existing short positions are closed first if Allow Short Exit is enabled.
  • Short entry – allowed when Allow Short Entry is enabled and the indicator turns downward (Value[Signal Shift - 1] > Value[Signal Shift - 2] and Value[Signal Shift] < Value[Signal Shift - 1]). Existing long positions are closed beforehand if Allow Long Exit is enabled.
  • Long exit – when in a long position and the short-entry condition triggers, the position is closed if Allow Long Exit is true.
  • Short exit – when in a short position and the long-entry condition triggers, the position is closed if Allow Short Exit is true.
  • All trades are executed with market orders using the volume specified in Order Volume. Protective stop-loss and take-profit brackets are attached automatically via StartProtection and remain active while the position is open.

Parameters

  • Candle Type – data type (timeframe or other candle description) used for indicator computation and trading decisions. Default is 4-hour candles.
  • Smoothing Method – averaging algorithm shared by all three smoothing stages (Simple, Exponential, Smoothed, Linear Weighted).
  • Momentum Depth – number of bars between the two price points that form raw momentum.
  • First/Second/Third Smoothing – lengths of the three averaging stages applied to both the momentum and its absolute value.
  • Signal Shift – number of already closed candles to look back when evaluating reversal patterns (minimum value is 1).
  • Momentum Price – applied price used for the non-delayed leg of the momentum calculation.
  • Reference Price – applied price used for the delayed comparison leg.
  • Allow Long Entry, Allow Short Entry – toggles to permit opening trades in each direction.
  • Allow Long Exit, Allow Short Exit – toggles controlling whether opposite signals close the respective positions.
  • Stop-Loss Points, Take-Profit Points – risk limits measured in price steps (Security.PriceStep). When set to zero the corresponding bracket is disabled.
  • Order Volume – absolute quantity used when sending market orders. The strategy also assigns this value to the base Strategy.Volume property.

Additional notes

  • The supported smoothing methods correspond to StockSharp indicators: Simple Moving Average, Exponential Moving Average, Smoothed Moving Average (RMA) and Weighted Moving Average.
  • The Demark price constant replicates the MT5 implementation by averaging price extremes and the candle close before adjusting the high/low distances.
  • Because calculations use only finished candles, the strategy reacts once per bar, matching the original EA behaviour that checked for new bars via IsNewBar.
  • Stop-Loss Points and Take-Profit Points are interpreted as multiples of the instrument price step to stay consistent with the point-based inputs of the original MQL5 strategy.
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;
	}
}