GitHub で見る

Exp BlauCMI 戦略

概要

この戦略はStockSharpの高レベルAPIを使用してMetaTrader 5エキスパートアドバイザーExp_BlauCMIを再現します。設定可能なローソク足シリーズ上でBlau Candle Momentum Index(CMI)という三重平滑化されたモメンタム比率を計算し、オシレーターのスイングに反応します。指標が下落後に上向きに転じた時にロング取引が開かれ、上昇後に下向きに転じた時にショート取引が開かれます。モジュールは実装を完全にイベント駆動で維持します。注文はローソク足が閉じた後にのみ送信されます。

インジケーターロジック

  1. Momentum PriceReference Priceを通じて2つの価格ソースが選択されます。生のモメンタムは最初の価格の現在値と2番目の価格の遅延値の差です。遅延はMomentum Depthによって制御されます。
  2. モメンタムとその絶対値は3つの連続した移動平均(First/Second/Third Smoothing)を通過します。各段階で同じ平均化メソッドが使用され、単純移動平均、指数移動平均、平滑化(RMA)および線形加重移動平均の中から選択できます。
  3. Blau CMIは100 * smoothedMomentum / smoothedAbsMomentumとして計算されます。インジケーターは第3平滑化段階が十分なバーを蓄積した後に取引シグナルを生成し始めます。
  4. Signal Shiftパラメーターは、反転を評価する前に戦略が何本の閉じたローソク足を遡って検査するかを決定します(値1はオリジナルEAを再現し、最後に閉じたバーを使用します)。

取引ルール

  • ロングエントリーAllow Long Entryが有効で、インジケーターシーケンスValue[Signal Shift - 1] < Value[Signal Shift - 2]に続いてValue[Signal Shift] > Value[Signal Shift - 1]が観察された場合に許可され、オシレーターが上向きに転じたことを意味します。Allow Short Exitが有効な場合、既存のショートポジションが最初に閉じられます。
  • ショートエントリーAllow Short Entryが有効で、インジケーターが下向きに転じた場合(Value[Signal Shift - 1] > Value[Signal Shift - 2]かつValue[Signal Shift] < Value[Signal Shift - 1])に許可されます。Allow Long Exitが有効な場合、既存のロングポジションが事前に閉じられます。
  • ロング決済 – ロングポジションにある時にショートエントリー条件が発動し、Allow Long Exitがtrueの場合にポジションが閉じられます。
  • ショート決済 – ショートポジションにある時にロングエントリー条件が発動し、Allow Short Exitがtrueの場合にポジションが閉じられます。
  • すべての取引はOrder Volumeで指定されたボリュームを使用した成行注文で実行されます。保護的なストップロスとテイクプロフィットのブラケットがStartProtectionを通じて自動的に付加され、ポジションがオープンの間はアクティブのままです。

パラメーター

  • Candle Type – インジケーター計算と取引判断に使用するデータタイプ(時間軸またはその他のローソク足説明)。デフォルトは4時間足です。
  • Smoothing Method – すべての3つの平滑化段階で共有される平均化アルゴリズム(単純、指数、平滑化、線形加重)。
  • Momentum Depth – 生のモメンタムを形成する2つの価格点の間のバー数。
  • First/Second/Third Smoothing – モメンタムとその絶対値の両方に適用される3つの平均化段階の長さ。
  • Signal Shift – 反転パターンを評価する際に遡る既に閉じたローソク足の数(最小値は1)。
  • Momentum Price – モメンタム計算の非遅延側に使用する適用価格。
  • Reference Price – 遅延比較側に使用する適用価格。
  • Allow Long EntryAllow Short Entry – 各方向での取引開始を許可するトグル。
  • Allow Long ExitAllow Short Exit – 反対のシグナルが各ポジションを閉じるかどうかを制御するトグル。
  • Stop-Loss PointsTake-Profit Points – 価格ステップ(Security.PriceStep)で測定したリスク制限。ゼロに設定すると対応するブラケットが無効になります。
  • Order Volume – 成行注文を送信する際の絶対数量。戦略はこの値をベースのStrategy.Volumeプロパティにも割り当てます。

追加注記

  • サポートされている平滑化メソッドはStockSharpのインジケーターに対応します:単純移動平均、指数移動平均、平滑化移動平均(RMA)および加重移動平均。
  • Demark価格定数は、高低の調整前に価格の極値とローソク足の終値を平均化することでMT5実装を再現します。
  • 計算は完成したローソク足のみを使用するため、戦略はバーごとに1回反応し、IsNewBarで新しいバーを確認したオリジナルEAの動作と一致します。
  • Stop-Loss PointsTake-Profit PointsはオリジナルMQL5戦略のポイントベースの入力と一貫性を保つために、銘柄の価格ステップの倍数として解釈されます。
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;
	}
}