在 GitHub 上查看

Blau Ergodic策略

该策略是 MQL5 平台 Exp_BlauErgodic 顾问在 StockSharp 上的移植版本。通过对动量及其绝对值进行三级 EMA 平滑, 构建 Blau Ergodic 振荡器的归一化主线与信号线,并提供三个与原版一致的信号模式。

默认订阅已完成的 4 小时K线。可以选择不同的价格来源(收盘价、开盘价、各种平均价),调整每一级平滑长度, 以及指定读取信号的柱索引 SignalBar。仓位规模由策略的 Volume 属性控制,可分别禁用多空开仓或平仓标志。 止损和止盈以点数设置,并通过 Security.PriceStep 转换成绝对价格。

信号模式

  • Breakdown:关注振荡器穿越零轴。指标由负转正时开多,由正转负时开空;当振荡器保持在相反的区域时 平掉现有仓位。
  • Twist:寻找斜率反转。如果上一根柱子仍在下行而最新柱子转为上行则出现多头信号;空头信号为相反情况。
  • CloudTwist:监控振荡器与信号线的交叉。穿越信号云向上时开多,跌回信号线下方时开空。

所有模式都以 SignalBar 指定的已完成柱(默认 1,即上一根完成的K线)为当前值,并结合更早的数值进行确认。 由于策略只处理收盘后的数据,请将 SignalBar 设为不小于 1

进出场规则

  • 做多AllowBuyEntry = true 且当前净头寸不为多头(Position <= 0)时,只要所选模式给出买入条件就会建仓。 如果存在空头敞口,系统会一次性买入 Volume + |Position| 以反向并建立多单。
  • 做空AllowSellEntry = true 且当前净头寸不为空头(Position >= 0)时,模式触发卖出条件就会建立空单, 同时平掉可能存在的多头仓位。
  • 平多:当模式给出反向信号或触发 StopLossPoints/TakeProfitPoints 时执行。被动退出会检查 AllowBuyExit,而由止损/止盈触发的强制退出会忽略该标志以确保保护单有效。
  • 平空:与平多逻辑相同,使用 AllowSellExit 及相应的止损/止盈距离。

参数

  • CandleType:订阅的K线类型(默认 4 小时)。
  • ModeBreakdownTwistCloudTwist 三种模式之一。
  • MomentumLength:原始动量差分的长度。
  • First/Second/ThirdSmoothingLength:动量级联 EMA 的长度。
  • SignalSmoothingLength:信号线 EMA 的长度。
  • SignalBar:用于读取信号的已完成柱索引(至少为 1)。
  • AppliedPrices:振荡器使用的价格来源(收盘价、开盘价、均价等)。
  • AllowBuyEntryAllowSellEntryAllowBuyExitAllowSellExit:分别控制多空的开平仓权限。
  • StopLossPointsTakeProfitPoints:以点数表示的止损/止盈距离(通过 Security.PriceStep 转换)。

该移植使用 StockSharp 的高级 API(SubscribeCandlesBind),保持 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;

namespace StockSharp.Samples.Strategies;



/// <summary>
/// Blau Ergodic oscillator strategy with multiple signal modes.
/// </summary>
public class BlauErgodicStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<BlauErgodicModes> _mode;
	private readonly StrategyParam<int> _momentumLength;
	private readonly StrategyParam<int> _firstSmoothingLength;
	private readonly StrategyParam<int> _secondSmoothingLength;
	private readonly StrategyParam<int> _thirdSmoothingLength;
	private readonly StrategyParam<int> _signalSmoothingLength;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<AppliedPrices> _appliedPrice;
	private readonly StrategyParam<bool> _allowBuyEntry;
	private readonly StrategyParam<bool> _allowSellEntry;
	private readonly StrategyParam<bool> _allowBuyExit;
	private readonly StrategyParam<bool> _allowSellExit;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _momEma1 = null!;
	private ExponentialMovingAverage _momEma2 = null!;
	private ExponentialMovingAverage _momEma3 = null!;
	private ExponentialMovingAverage _absMomEma1 = null!;
	private ExponentialMovingAverage _absMomEma2 = null!;
	private ExponentialMovingAverage _absMomEma3 = null!;
	private ExponentialMovingAverage _signal = null!;

	private readonly List<decimal> _priceHistory = new();
	private readonly List<decimal> _mainHistory = new();
	private readonly List<decimal?> _signalHistory = new();

	private decimal _entryPrice;

	/// <summary>
	/// Trading candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Mode that defines signal detection.
	/// </summary>
	public BlauErgodicModes Mode
	{
		get => _mode.Value;
		set => _mode.Value = value;
	}

	/// <summary>
	/// Momentum lookback length.
	/// </summary>
	public int MomentumLength
	{
		get => _momentumLength.Value;
		set => _momentumLength.Value = value;
	}

	/// <summary>
	/// First EMA smoothing length for momentum streams.
	/// </summary>
	public int FirstSmoothingLength
	{
		get => _firstSmoothingLength.Value;
		set => _firstSmoothingLength.Value = value;
	}

	/// <summary>
	/// Second EMA smoothing length for momentum streams.
	/// </summary>
	public int SecondSmoothingLength
	{
		get => _secondSmoothingLength.Value;
		set => _secondSmoothingLength.Value = value;
	}

	/// <summary>
	/// Third EMA smoothing length for momentum streams.
	/// </summary>
	public int ThirdSmoothingLength
	{
		get => _thirdSmoothingLength.Value;
		set => _thirdSmoothingLength.Value = value;
	}

	/// <summary>
	/// EMA length applied to the signal line.
	/// </summary>
	public int SignalSmoothingLength
	{
		get => _signalSmoothingLength.Value;
		set => _signalSmoothingLength.Value = value;
	}

	/// <summary>
	/// Number of closed candles used to read signals.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Price source used inside the indicator.
	/// </summary>
	public AppliedPrices AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	/// <summary>
	/// Allows opening long positions.
	/// </summary>
	public bool AllowBuyEntry
	{
		get => _allowBuyEntry.Value;
		set => _allowBuyEntry.Value = value;
	}

	/// <summary>
	/// Allows opening short positions.
	/// </summary>
	public bool AllowSellEntry
	{
		get => _allowSellEntry.Value;
		set => _allowSellEntry.Value = value;
	}

	/// <summary>
	/// Allows closing long positions on indicator signals.
	/// </summary>
	public bool AllowBuyExit
	{
		get => _allowBuyExit.Value;
		set => _allowBuyExit.Value = value;
	}

	/// <summary>
	/// Allows closing short positions on indicator signals.
	/// </summary>
	public bool AllowSellExit
	{
		get => _allowSellExit.Value;
		set => _allowSellExit.Value = value;
	}

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

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

	/// <summary>
	/// Constructor.
	/// </summary>
	public BlauErgodicStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for calculations", "General");

		_mode = Param(nameof(Mode), BlauErgodicModes.Twist)
		.SetDisplay("Mode", "Signal interpretation mode", "Trading");

		_momentumLength = Param(nameof(MomentumLength), 2)
		.SetGreaterThanZero()
		.SetDisplay("Momentum Length", "Momentum lookback for Blau Ergodic", "Indicator");

		_firstSmoothingLength = Param(nameof(FirstSmoothingLength), 20)
		.SetGreaterThanZero()
		.SetDisplay("First Smoothing", "First EMA smoothing length", "Indicator");

		_secondSmoothingLength = Param(nameof(SecondSmoothingLength), 5)
		.SetGreaterThanZero()
		.SetDisplay("Second Smoothing", "Second EMA smoothing length", "Indicator");

		_thirdSmoothingLength = Param(nameof(ThirdSmoothingLength), 3)
		.SetGreaterThanZero()
		.SetDisplay("Third Smoothing", "Third EMA smoothing length", "Indicator");

		_signalSmoothingLength = Param(nameof(SignalSmoothingLength), 3)
		.SetGreaterThanZero()
		.SetDisplay("Signal Smoothing", "EMA length for signal line", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
		.SetGreaterThanZero()
		.SetDisplay("Signal Bar", "Completed bars back to evaluate", "Trading");

		_appliedPrice = Param(nameof(AppliedPrices), AppliedPrices.Close)
		.SetDisplay("Applied Price", "Price source for calculations", "Indicator");

		_allowBuyEntry = Param(nameof(AllowBuyEntry), true)
		.SetDisplay("Allow Buy Entry", "Allow opening long positions", "Trading");

		_allowSellEntry = Param(nameof(AllowSellEntry), true)
		.SetDisplay("Allow Sell Entry", "Allow opening short positions", "Trading");

		_allowBuyExit = Param(nameof(AllowBuyExit), true)
		.SetDisplay("Allow Buy Exit", "Allow closing long positions", "Trading");

		_allowSellExit = Param(nameof(AllowSellExit), true)
		.SetDisplay("Allow Sell Exit", "Allow closing short positions", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
		.SetDisplay("Stop Loss", "Protective stop loss distance", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
		.SetDisplay("Take Profit", "Profit target distance", "Risk");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_priceHistory.Clear();
		_mainHistory.Clear();
		_signalHistory.Clear();
		_entryPrice = default;
	}

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

		// Initialize EMA cascades for momentum and absolute momentum streams.
		_momEma1 = new ExponentialMovingAverage { Length = FirstSmoothingLength };
		_momEma2 = new ExponentialMovingAverage { Length = SecondSmoothingLength };
		_momEma3 = new ExponentialMovingAverage { Length = ThirdSmoothingLength };

		_absMomEma1 = new ExponentialMovingAverage { Length = FirstSmoothingLength };
		_absMomEma2 = new ExponentialMovingAverage { Length = SecondSmoothingLength };
		_absMomEma3 = new ExponentialMovingAverage { Length = ThirdSmoothingLength };

		_signal = new ExponentialMovingAverage { Length = SignalSmoothingLength };

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

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

		// Store price history for momentum calculation.
		var price = GetAppliedPrice(candle);
		_priceHistory.Add(price);
		TrimHistory(_priceHistory, MomentumLength + SignalBar + 10);

		if (MomentumLength <= 0)
			return;

		var backShift = MomentumLength - 1;
		if (_priceHistory.Count <= backShift)
			return;

		var referenceIndex = _priceHistory.Count - 1 - backShift;
		var referencePrice = _priceHistory[referenceIndex];
		var momentum = price - referencePrice;
		var absMomentum = Math.Abs(momentum);

		// Process cascaded EMA filters for momentum and absolute momentum.
		var time = candle.ServerTime;

		var mom1 = _momEma1.Process(new DecimalIndicatorValue(_momEma1, momentum, time) { IsFinal = true });
		var abs1 = _absMomEma1.Process(new DecimalIndicatorValue(_absMomEma1, absMomentum, time) { IsFinal = true });

		if (mom1.IsEmpty || abs1.IsEmpty)
			return;

		var mom2 = _momEma2.Process(new DecimalIndicatorValue(_momEma2, mom1.ToDecimal(), time) { IsFinal = true });
		var abs2 = _absMomEma2.Process(new DecimalIndicatorValue(_absMomEma2, abs1.ToDecimal(), time) { IsFinal = true });

		if (mom2.IsEmpty || abs2.IsEmpty)
			return;

		var mom3 = _momEma3.Process(new DecimalIndicatorValue(_momEma3, mom2.ToDecimal(), time) { IsFinal = true });
		var abs3 = _absMomEma3.Process(new DecimalIndicatorValue(_absMomEma3, abs2.ToDecimal(), time) { IsFinal = true });

		if (mom3.IsEmpty || abs3.IsEmpty)
			return;

		var smoothedMomentum = mom3.ToDecimal();
		var smoothedAbsMomentum = abs3.ToDecimal();

		var main = smoothedAbsMomentum == 0m ? 0m : 100m * smoothedMomentum / smoothedAbsMomentum;

		var signalValue = _signal.Process(new DecimalIndicatorValue(_signal, main, time) { IsFinal = true });
		decimal? signal = null;
		if (!signalValue.IsEmpty)
			signal = signalValue.ToDecimal();

		AppendIndicatorHistory(main, signal);

		EvaluateSignals(candle);
	}

	private void EvaluateSignals(ICandleMessage candle)
	{

		var currentIndex = SignalBar - 1;
		if (currentIndex < 0)
			return;

		if (!TryGetMainValue(currentIndex, out var currentMain))
			return;

		var buyOpen = false;
		var sellOpen = false;
		var buyClose = false;
		var sellClose = false;

		switch (Mode)
		{
			case BlauErgodicModes.Breakdown:
			{
				if (!TryGetMainValue(currentIndex + 1, out var previousMain))
					return;

				// Close shorts when histogram stays above zero and longs when it stays below zero.
				if (AllowSellExit && currentMain > 0m)
					sellClose = true;

				if (AllowBuyExit && currentMain < 0m)
					buyClose = true;

				if (AllowBuyEntry && previousMain <= 0m && currentMain > 0m)
					buyOpen = true;

				if (AllowSellEntry && previousMain >= 0m && currentMain < 0m)
					sellOpen = true;

				break;
			}
			case BlauErgodicModes.Twist:
			{
				if (!TryGetMainValue(currentIndex + 1, out var previousMain) ||
				!TryGetMainValue(currentIndex + 2, out var olderMain))
					return;

				// Detect turning points by comparing slope changes.
				if (AllowSellExit && previousMain < currentMain)
					sellClose = true;

				if (AllowBuyExit && previousMain > currentMain)
					buyClose = true;

				if (AllowBuyEntry && olderMain > previousMain && previousMain < currentMain)
					buyOpen = true;

				if (AllowSellEntry && olderMain < previousMain && previousMain > currentMain)
					sellOpen = true;

				break;
			}
			case BlauErgodicModes.CloudTwist:
			{
				if (!TryGetMainValue(currentIndex + 1, out var previousMain) ||
				!TryGetSignalValue(currentIndex, out var currentSignal) ||
				!TryGetSignalValue(currentIndex + 1, out var previousSignal))
					return;

				// Close when main line crosses the signal line.
				if (AllowSellExit && currentMain > currentSignal)
					sellClose = true;

				if (AllowBuyExit && currentMain < currentSignal)
					buyClose = true;

				if (AllowBuyEntry && previousMain <= previousSignal && currentMain > currentSignal)
					buyOpen = true;

				if (AllowSellEntry && previousMain >= previousSignal && currentMain < currentSignal)
					sellOpen = true;

				break;
			}
		}

		var (closeLongByStops, closeShortByStops) = EvaluateStops(candle);

		var forceBuyClose = closeLongByStops;
		var forceSellClose = closeShortByStops;

		if (closeLongByStops)
			buyClose = true;

		if (closeShortByStops)
			sellClose = true;

		ExecuteOrders(candle, buyOpen, sellOpen, buyClose, sellClose, forceBuyClose, forceSellClose);
	}

	private (bool closeLong, bool closeShort) EvaluateStops(ICandleMessage candle)
	{
		var closeLong = false;
		var closeShort = false;

		var priceStep = Security?.PriceStep ?? 0m;
		var stopLossDistance = priceStep > 0m && StopLossPoints > 0 ? StopLossPoints * priceStep : 0m;
		var takeProfitDistance = priceStep > 0m && TakeProfitPoints > 0 ? TakeProfitPoints * priceStep : 0m;

		// Evaluate protective levels against the current candle range.
		if (Position > 0)
		{
			if (stopLossDistance > 0m && candle.LowPrice <= _entryPrice - stopLossDistance)
				closeLong = true;

			if (takeProfitDistance > 0m && candle.HighPrice >= _entryPrice + takeProfitDistance)
				closeLong = true;
		}
		else if (Position < 0)
		{
			if (stopLossDistance > 0m && candle.HighPrice >= _entryPrice + stopLossDistance)
				closeShort = true;

			if (takeProfitDistance > 0m && candle.LowPrice <= _entryPrice - takeProfitDistance)
				closeShort = true;
		}

		return (closeLong, closeShort);
	}

	private void ExecuteOrders(ICandleMessage candle, bool buyOpen, bool sellOpen, bool buyClose, bool sellClose, bool forceBuyClose, bool forceSellClose)
	{
		if (((buyClose && AllowBuyExit) || forceBuyClose) && Position > 0)
		{
			// Close existing long position.
			SellMarket(Position);
			_entryPrice = 0m;
		}

		if (((sellClose && AllowSellExit) || forceSellClose) && Position < 0)
		{
			// Close existing short position.
			BuyMarket(-Position);
			_entryPrice = 0m;
		}

		if (buyOpen && AllowBuyEntry && Position <= 0)
		{
			// Reverse any short exposure and open a new long.
			var volume = Volume + Math.Abs(Position);
			BuyMarket(volume);
			_entryPrice = candle.ClosePrice;
		}

		if (sellOpen && AllowSellEntry && Position >= 0)
		{
			// Reverse any long exposure and open a new short.
			var volume = Volume + Math.Abs(Position);
			SellMarket(volume);
			_entryPrice = candle.ClosePrice;
		}
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		return AppliedPrice switch
		{
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + candle.ClosePrice + candle.ClosePrice) / 4m,
			AppliedPrices.Simple => (candle.OpenPrice + candle.ClosePrice) / 2m,
			AppliedPrices.Quarter => (candle.HighPrice + candle.LowPrice + candle.OpenPrice + candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
	}

	private void AppendIndicatorHistory(decimal main, decimal? signal)
	{
		_mainHistory.Add(main);
		_signalHistory.Add(signal);

		var maxSize = Math.Max(SignalBar + 5, 10);
		TrimHistory(_mainHistory, maxSize);
		TrimHistory(_signalHistory, maxSize);
	}

	private static void TrimHistory<T>(IList<T> values, int maxSize)
	{
		while (values.Count > maxSize)
			values.RemoveAt(0);
	}

	private bool TryGetMainValue(int shift, out decimal value)
	{
		value = default;
		var index = _mainHistory.Count - 1 - shift;
		if (index < 0 || index >= _mainHistory.Count)
			return false;

		value = _mainHistory[index];
		return true;
	}

	private bool TryGetSignalValue(int shift, out decimal value)
	{
		value = default;
		var index = _signalHistory.Count - 1 - shift;
		if (index < 0 || index >= _signalHistory.Count)
			return false;

		var raw = _signalHistory[index];
		if (raw is null)
			return false;

		value = raw.Value;
		return true;
	}

	/// <summary>
	/// Trading modes supported by the strategy.
	/// </summary>
	public enum BlauErgodicModes
	{
		Breakdown,
		Twist,
		CloudTwist,
	}

	/// <summary>
	/// Price types available for indicator calculation.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Simple,
		Quarter,
	}
}