在 GitHub 上查看

BSS Triple EMA Separation 策略

概述

BSS Triple EMA Separation 策略 是 MetaTrader 5 专家顾问 “BSS 1_0”(MQL ID 20591)的 StockSharp 版本。算法同时监控三个周期逐渐递增的移动平均线,并要求它们之间至少保持指定的最小间距。一旦满足条件,策略便顺势开仓,并对连续入场之间的间隔和最大仓位规模进行控制。

本实现保持了原始 EA 的核心逻辑,并通过 StrategyParam 对象公开所有关键参数。按照要求,代码注释和主要文档均使用英文撰写。

交易逻辑

  1. 根据 CandleType 参数订阅单一时间框架的 K 线数据,并计算三条移动平均线(快速、中速、慢速)。每条平均线都可以选择不同的平滑方式(简单、指数、平滑或线性加权)。
  2. 做多条件(在收盘完毕的 K 线上检查):
    • 慢速 MA - 中速 MA >= MinimumDistance
    • 中速 MA - 快速 MA >= MinimumDistance
  3. 做空条件 与上述相反:
    • 快速 MA - 中速 MA >= MinimumDistance
    • 中速 MA - 慢速 MA >= MinimumDistance
  4. 在发送委托之前需要满足以下前置条件:
    • 所有指标均已形成,策略处于允许交易的状态(IsFormedAndOnlineAndAllowTrading)。
    • 自上一次入场以来已超过 MinimumPauseSeconds 指定的秒数。
    • 新增仓位不会突破 MaxPositions 限定的最大净持仓。
  5. 当出现反向信号时,会先平掉相反方向的仓位,随后在下一根满足条件的 K 线上考虑开仓,这一点与原始 MQL 策略的行为保持一致。
  6. 每当产生新的入场或加仓,策略都会记录成交时间,以确保后续交易遵守冷却时间。

该策略不包含固定止损或止盈,风险控制主要依靠距离过滤、交易间隔和最大仓位限制。

参数说明

参数 默认值 说明
OrderVolume 0.1 单笔下单量。策略的净头寸被限制在 OrderVolume * MaxPositions 之内。
MaxPositions 2 同方向可同时持有的最大总量(以手数/批数计)。
MinimumDistance 0.0005 相邻两条均线之间所需的最小价格差。请根据标的设置合适的值(例如 5 位报价的 EURUSD 中 0.0005 等于 5 个点)。
MinimumPauseSeconds 600 新增仓位之间的最小等待时间(秒)。平仓不会重置计时器,只有新的入场会更新。
FirstMaPeriod 5 快速移动平均线的周期,必须小于 SecondMaPeriod
FirstMaMethod Exponential 快速均线的平滑方式(Simple、Exponential、Smoothed、LinearWeighted)。
SecondMaPeriod 25 中速移动平均线的周期,必须小于 ThirdMaPeriod
SecondMaMethod Exponential 中速均线的平滑方式。
ThirdMaPeriod 125 慢速移动平均线的周期。
ThirdMaMethod Exponential 慢速均线的平滑方式。
CandleType 1 分钟 用于计算指标和判断信号的 K 线类型。

实现细节

  • 使用 StockSharp 的高层 API:SubscribeCandles 负责订阅数据流,Bind 将数据同时传递给三个均线指标及信号处理函数。
  • 移动平均线在 OnStarted 中根据参数选择相应的实现,默认配置(三条指数均线、取收盘价)与原始 EA 完全一致。
  • 调用 StartProtection() 激活 StockSharp 的内置仓位保护机制。
  • 重写 OnPositionChanged,在净头寸增加时记录成交时间,从而实现与 MQL 策略相同的冷却逻辑。
  • 在开新仓前会先关闭反向仓位,确保净头寸不会在同一时刻直接从多头转为空头或反之。

使用建议

  1. 根据交易品种的最小报价单位调整 MinimumDistance
    • EURUSD(5 位报价):0.0005 ≈ 5 点。
    • USDJPY(3 位报价):0.05 ≈ 5 点。
  2. 结合不同时间框架和市场状态调整三条均线的周期及平滑方式。
  3. 在较慢的时间框架上可以适当增加 MinimumPauseSeconds,以避免过度交易;在较快时间框架上则可以适度缩短。
  4. OrderVolume 联合调整 MaxPositions,确保实际头寸规模符合资金管理计划。

与原版 EA 的差异

  • 原 EA 支持选择不同的价格类型(开盘价、高价、低价等)。当前移植版本仅使用收盘价,这与默认配置一致。
  • 策略按照净头寸模型工作:多头持仓为正,空头持仓为负。当净头寸达到 MaxPositions 限制时,将不会继续加仓,直到仓位被部分或全部平掉,这与原始 MQL 实现的仓位计数逻辑一致。

通过上述配置,您可以在 StockSharp 生态中复现 BSS 策略的主要思想,并根据需要进一步叠加风险控制或其他分析模块。

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;

public class BssTripleEmaSeparationStrategy : Strategy
{
	public enum MaMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted,
	}

	// Small epsilon used to compare decimal volumes without floating point noise.
	private readonly StrategyParam<decimal> _volumeTolerance;

	// User configurable parameters.
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _minimumDistance;
	private readonly StrategyParam<int> _minimumPauseSeconds;
	private readonly StrategyParam<int> _firstMaPeriod;
	private readonly StrategyParam<int> _secondMaPeriod;
	private readonly StrategyParam<int> _thirdMaPeriod;
	private readonly StrategyParam<MaMethods> _firstMaMethod;
	private readonly StrategyParam<MaMethods> _secondMaMethod;
	private readonly StrategyParam<MaMethods> _thirdMaMethod;
	private readonly StrategyParam<DataType> _candleType;

	// Indicator instances created according to the selected parameters.
	private IIndicator _firstMa = null!;
	private IIndicator _secondMa = null!;
	private IIndicator _thirdMa = null!;

	// Timestamp of the last position entry used to enforce the pause between trades.
	private DateTimeOffset? _lastEntryTime;

	/// <summary>
	/// Tolerance used when comparing accumulated volume values.
	/// </summary>
	public decimal VolumeTolerance
	{
		get => _volumeTolerance.Value;
		set => _volumeTolerance.Value = value;
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	public decimal MinimumDistance
	{
		get => _minimumDistance.Value;
		set => _minimumDistance.Value = value;
	}

	public int MinimumPauseSeconds
	{
		get => _minimumPauseSeconds.Value;
		set => _minimumPauseSeconds.Value = value;
	}

	public int FirstMaPeriod
	{
		get => _firstMaPeriod.Value;
		set => _firstMaPeriod.Value = value;
	}

	public int SecondMaPeriod
	{
		get => _secondMaPeriod.Value;
		set => _secondMaPeriod.Value = value;
	}

	public int ThirdMaPeriod
	{
		get => _thirdMaPeriod.Value;
		set => _thirdMaPeriod.Value = value;
	}

	public MaMethods FirstMaMethod
	{
		get => _firstMaMethod.Value;
		set => _firstMaMethod.Value = value;
	}

	public MaMethods SecondMaMethod
	{
		get => _secondMaMethod.Value;
		set => _secondMaMethod.Value = value;
	}

	public MaMethods ThirdMaMethod
	{
		get => _thirdMaMethod.Value;
		set => _thirdMaMethod.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public BssTripleEmaSeparationStrategy()
	{
		_volumeTolerance = Param(nameof(VolumeTolerance), 1e-8m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Tolerance", "Tolerance when comparing volume values", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume used for each entry order", "Trading");

		_maxPositions = Param(nameof(MaxPositions), 2)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum simultaneous entries per direction", "Risk");

		_minimumDistance = Param(nameof(MinimumDistance), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Distance", "Minimum price gap between moving averages", "Signals");

		_minimumPauseSeconds = Param(nameof(MinimumPauseSeconds), 600)
			.SetNotNegative()
			.SetDisplay("Minimum Pause (sec)", "Pause between new entries in seconds", "Risk");

		_firstMaPeriod = Param(nameof(FirstMaPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("First MA Period", "Period for the fastest moving average", "Indicators");

		_firstMaMethod = Param(nameof(FirstMaMethod), MaMethods.Exponential)
			.SetDisplay("First MA Method", "Smoothing method for the fastest moving average", "Indicators");

		_secondMaPeriod = Param(nameof(SecondMaPeriod), 25)
			.SetGreaterThanZero()
			.SetDisplay("Second MA Period", "Period for the medium moving average", "Indicators");

		_secondMaMethod = Param(nameof(SecondMaMethod), MaMethods.Exponential)
			.SetDisplay("Second MA Method", "Smoothing method for the medium moving average", "Indicators");

		_thirdMaPeriod = Param(nameof(ThirdMaPeriod), 125)
			.SetGreaterThanZero()
			.SetDisplay("Third MA Period", "Period for the slowest moving average", "Indicators");

		_thirdMaMethod = Param(nameof(ThirdMaMethod), MaMethods.Exponential)
			.SetDisplay("Third MA Method", "Smoothing method for the slowest moving average", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_lastEntryTime = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (FirstMaPeriod >= SecondMaPeriod)
			throw new InvalidOperationException("First MA period must be less than second MA period.");

		if (SecondMaPeriod >= ThirdMaPeriod)
			throw new InvalidOperationException("Second MA period must be less than third MA period.");

		_firstMa = CreateMovingAverage(FirstMaMethod, FirstMaPeriod);
		_secondMa = CreateMovingAverage(SecondMaMethod, SecondMaPeriod);
		_thirdMa = CreateMovingAverage(ThirdMaMethod, ThirdMaPeriod);

		_lastEntryTime = null;

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_firstMa, _secondMa, _thirdMa, ProcessCandle).Start();

	}

	private static IIndicator CreateMovingAverage(MaMethods method, int period)
	{
		return method switch
		{
			MaMethods.Simple => new SimpleMovingAverage { Length = period },
			MaMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MaMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
			_ => new ExponentialMovingAverage { Length = period },
		};
	}

	private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal thirdValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_firstMa.IsFormed || !_secondMa.IsFormed || !_thirdMa.IsFormed)
			return;

		var minDistance = MinimumDistance;

		var longSpreadOk = thirdValue - secondValue >= minDistance && secondValue - firstValue >= minDistance;
		var shortSpreadOk = firstValue - secondValue >= minDistance && secondValue - thirdValue >= minDistance;

		if (!longSpreadOk && !shortSpreadOk)
			return;

		var time = candle.OpenTime;

		if (longSpreadOk)
		{
			if (TryCloseOppositePositions(true))
				return;

			if (CanEnterPosition(time, true))
			{
				BuyMarket(OrderVolume);
				_lastEntryTime = time;
			}

			return;
		}

		if (shortSpreadOk)
		{
			if (TryCloseOppositePositions(false))
				return;

			if (CanEnterPosition(time, false))
			{
				SellMarket(OrderVolume);
				_lastEntryTime = time;
			}
		}
	}

	private bool CanEnterPosition(DateTimeOffset time, bool isLong)
	{
		// Trading is allowed only when the strategy is ready, the pause elapsed, and exposure stays within bounds.

		if (!IsPauseElapsed(time))
			return false;

		var targetPosition = Position + (isLong ? OrderVolume : -OrderVolume);
		var maxExposure = MaxPositions * OrderVolume;

		return Math.Abs(targetPosition) <= maxExposure + VolumeTolerance;
	}

	private bool IsPauseElapsed(DateTimeOffset time)
	{
		var pauseSeconds = MinimumPauseSeconds;

		if (pauseSeconds <= 0)
			return true;

		if (_lastEntryTime is null)
			return true;

		return time - _lastEntryTime.Value >= TimeSpan.FromSeconds(pauseSeconds);
	}

	private bool TryCloseOppositePositions(bool isLong)
	{
		// Close active trades in the opposite direction before opening a new position.
		if (isLong)
		{
			if (Position < -VolumeTolerance)
			{
				BuyMarket(Math.Abs(Position));
				return true;
			}
		}
		else
		{
			if (Position > VolumeTolerance)
			{
				SellMarket(Position);
				return true;
			}
		}

		return false;
	}

}