GitHub で見る

BSS Triple EMA分離戦略

概要

BSS Triple EMA分離戦略は、MetaTrader 5エキスパートアドバイザー「BSS 1_0」(MQL ID 20591)のStockSharpポートです。このアプローチは、増大するルックバックウィンドウを持つ3つの移動平均を監視し、それらが少なくとも設定可能な距離だけ広がるのを待ちます。速い、中間、遅い平均が適切に分離されると、戦略は合計ポジションサイズの上限とフィル間のクールダウンを尊重しながらトレンドの方向にエントリーします。

この実装はオリジナルロボットのコア動作を維持しながら、StockSharpのStrategyParamオブジェクトを通じて設定を公開します。すべてのコメントとドキュメントは要求に応じて英語で書かれています。

取引ロジック

  1. CandleTypeパラメーターで定義された単一のローソク足ストリームにサブスクライブし、3つの移動平均(速い、中間、遅い)を計算します。各平均は異なる平滑化メソッドを使用できます(単純、指数、平滑化、または線形加重)。
  2. 完成したローソク足でロングセットアップには以下の条件が満たされる必要があります:
    • 遅いMA - 中間MA >= MinimumDistance
    • 中間MA - 速いMA >= MinimumDistance
  3. ショートセットアップには逆の分離が必要です:
    • 速いMA - 中間MA >= MinimumDistance
    • 中間MA - 遅いMA >= MinimumDistance
  4. 取引を開く前に戦略は以下を確認します:
    • すべてのインジケーターが完全に形成され、戦略が取引を許可されています(IsFormedAndOnlineAndAllowTrading)。
    • 最後のエントリーからの一時停止(MinimumPauseSeconds)が経過しました。
    • 新しいロットを追加してもMaxPositionsエクスポージャー制限に違反しません。
  5. エントリーシグナルで戦略はまず反対方向に開いているポジションをクローズします。次のローソク足の後にのみ新しい方向でのポジションのオープンを検討します。これはオリジナルのMQL EAの動作を反映しています。
  6. 新しいポジションが開かれるかスケールされると、エントリー間のクールダウンを強制するためにフィル時間が保存されます。

自動ストップロスやテイクプロフィットは使用されません。リスク管理は距離フィルター、取引間の一時停止、各方向で許可されるロットの最大数によって達成されます。

パラメータ

パラメータ デフォルト 説明
OrderVolume 0.1 各エントリー注文に使用されるボリューム。ネットポジションはOrderVolume * MaxPositionsに制限されています。
MaxPositions 2 同時に保持できる最大ロット数(方向ごと)。
MinimumDistance 0.0005 隣接する移動平均間に必要な最小価格差。銘柄に適した値を選択してください(5桁FXペアの場合、0.0005は5 pipに等しい)。
MinimumPauseSeconds 600 新しいエントリー間のクールダウン(秒)。取引のクローズはタイマーをリセットしません。エントリーのみがリセットします。
FirstMaPeriod 5 最も速い移動平均の期間。SecondMaPeriodより厳密に小さくなければなりません。
FirstMaMethod Exponential 速い移動平均に使用される平滑化メソッド(Simple、Exponential、Smoothed、LinearWeighted)。
SecondMaPeriod 25 中間移動平均の期間。ThirdMaPeriodより厳密に小さくなければなりません。
SecondMaMethod Exponential 中間移動平均に使用される平滑化メソッド。
ThirdMaPeriod 125 遅い移動平均の期間。
ThirdMaMethod Exponential 遅い移動平均に使用される平滑化メソッド。
CandleType 1分時間軸 インジケーター計算とシグナル評価に使用されるローソク足データソース。

実装ノート

  • StockSharpの高レベルAPIが使用されます:SubscribeCandlesがデータをストリームし、.Bindが移動平均とシグナルハンドラーに同時にフィードします。
  • 移動平均は選択されたメソッドに従って戦略開始時にインスタンス化されます。デフォルト設定はオリジナルEA(終値での3つの指数MA)と一致します。
  • StartProtection()はStockSharpが提供する組み込みポジション監視ツールを有効にするために呼び出されます。
  • 戦略はエントリーにタイムスタンプを付けるためにOnPositionChangedをオーバーライドします。このタイムスタンプはMetaTraderバージョンのクールダウン動作を維持するためにMinimumPauseSecondsと比較されます。
  • 反対のポジションは新しいものを検討する前にフラット化され、ネットエクスポージャーがまずゼロを通過せずにサインが変わることがないことを保証します。これはすべてのショートポジションがロングを開く前にクローズされたオリジナル実装と同様です。

使用ガイドライン

  1. 銘柄を選択し、そのティックサイズがMinimumDistance値に反映されていることを確認してください。例えば:
    • EURUSD(5桁価格):0.0005は5 pipに等しい。
    • USDJPY(3桁価格):0.05は5 pipに等しい。
  2. ターゲットとする市場体制に合わせて移動平均の期間とメソッドを調整してください。
  3. 過剰取引を避けるために遅い時間軸でMinimumPauseSecondsを増加させるか、市場構造が頻繁なエントリーを許可する場合は低い時間軸で減少させてください。
  4. リスクプランにエクスポージャーを合わせるために、ブローカーのコントラクトサイズと組み合わせて異なるMaxPositions値をテストしてください。

MQLバージョンと比較した制限

  • MetaTraderのエキスパートは代替価格ソース(始値、高値、安値など)の選択を許可していました。StockSharpポートは現在終値のみで動作します。これはオリジナルロボットのデフォルト設定と一致します。
  • ポートはネットポジションモデルを使用します(ロングに正、ショートに負)。MaxPositionsに達すると、エクスポージャーが削減されるまで追加のロットは追加されません。これはオリジナルのポジションごとカウンターの効果を再現します。

これらの考慮事項を踏まえて、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;
	}

}