GitHub で見る

Universal MA Cross戦略

概要

Universal MA Cross戦略は、オリジナルのMQL5エキスパートアドバイザー「UniversalMACrossEA」をStockSharpの高レベル戦略フレームワークに直接変換したものです。このアルゴリズムは、異なる計算方法と価格ソースで設定できる高速と低速の移動平均線を比較します。オプションのフィルターにより、シグナルの確認方法、取引の即時反転、リスク管理の実行方法、戦略が取引を許可される時間を制御します。

取引ロジック

インジケーター処理

  • 選択したキャンドルシリーズで2本の移動平均線が計算されます。各平均は独自の期間、平滑化方法(SMA、EMA、SMMA、またはLWMA)、価格タイプ(終値、始値、高値、安値、中値、典型値、または加重値)を使用できます。
  • MinCrossDistanceパラメーターは、クロスオーバーバーで高速と低速の平均が少なくとも指定した価格単位数だけ乖離することを要求します。
  • ConfirmedOnEntryが有効な場合、クロスオーバーは前の完成したバーで検証されます(元のEAでバーインデックス2と1を使用することに相当)。無効の場合、現在の完成したバーが前のバーと比較され、MQLバージョンの「ティックモード」動作を再現します。
  • ReverseConditionを設定すると、インジケーター設定を変更せずにルールを反転できるよう、強気と弱気のシグナルが入れ替わります。

エントリー条件

  1. ロングエントリーには、高速平均が低速平均を少なくともMinCrossDistanceだけ上回る必要があります。ショートエントリーには、高速平均がその距離だけ低速平均を下回る必要があります。
  2. StopAndReverseが有効で逆のシグナルが来た場合、新しい注文を検討する前にアクティブなポジションが閉じられます。
  3. OneEntryPerBarが真の場合、戦略は最後のエントリーのバー時間を記憶し、同じキャンドル中に別の取引を開くことを拒否します。
  4. 各注文のボリュームはVolumeパラメーターで設定されます。

ポジション管理

  • ストップロスとテイクプロフィットのレベルは価格単位で測定されます。PureSarが真の場合は無視され、元のエキスパートの「Pure SAR」モードに一致します。
  • トレーリングストップロジックは、エントリー価格からTrailingStop + TrailingStepだけ価格が動いた後に起動します。TrailingStepポイント以上の追加の動きがあるたびに、指定されたTrailingStop距離だけストップが引き締まります。トレーリングは「Pure SAR」モードでは機能しません。
  • 保護レベルは完成した各キャンドルで監視されます。キャンドルの範囲がストップロスまたはテイクプロフィットレベルに違反した場合、ポジションは成行注文で閉じられます。

セッションフィルター

  • UseHourTradeが有効な場合、キャンドルの開始時間がStartHourEndHour(両端を含む)の間にある場合にのみ戦略が取引します。トレーリングストップ管理はその間隔の外でも継続して実行されますが、新しいエントリーやストップアンドリバースアクションは実行されません。

パラメーター

パラメーター 説明
FastMaPeriod, SlowMaPeriod 高速と低速の移動平均線の期間。
FastMaType, SlowMaType 移動平均メソッド:Simple、Exponential、Smoothed(RMA)、またはLinear Weighted。
FastPriceType, SlowPriceType 平均に入力される価格ソース。
StopLoss, TakeProfit 絶対価格単位での保護距離。無効にするには0に設定。
TrailingStop, TrailingStep トレーリングストップオフセットとストップを移動する前に必要な最小追加移動。
MinCrossDistance クロスオーバーバーでの平均間の最小距離。
ReverseCondition 強気と弱気のルールを入れ替える。
ConfirmedOnEntry 検証に完成したバーのみを使用する。
OneEntryPerBar キャンドルあたり最大1回のエントリーを許可する。
StopAndReverse 現在のポジションを閉じ、逆のシグナルで反転する。
PureSar ストップロス、テイクプロフィット、トレーリングロジックを無効にする。
UseHourTrade, StartHour, EndHour 取引セッションの時間フィルター(時間0–23)。
Volume 各ポジションの注文ボリューム。
CandleType 計算に使用するキャンドルデータタイプ。

変換に関する注意

  • StockSharpの戦略は生のティックイベントではなく確定したキャンドルで動作するため、保護注文はキャンドルの高値と安値を確認することで内部的に処理されます。これは高レベルAPIの範囲内で元のエキスパートの動作を反映します。
  • トレーリングストップ調整はMQL実装に従い、ストップが移動される前にTrailingStop + TrailingStepの動きを必要とします。
  • 要求に応じて、この変換にはPythonバージョンは提供されません。
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>
/// Universal moving average crossover strategy converted from the original MQL version.
/// The strategy trades based on a fast and a slow moving average with optional signal confirmation,
/// stop-and-reverse behaviour, trailing stop management and time filtering.
/// </summary>
public class UniversalMaCrossStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MovingAverageMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Price sources that can feed the moving averages.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<MovingAverageMethods> _fastMaType;
	private readonly StrategyParam<MovingAverageMethods> _slowMaType;
	private readonly StrategyParam<AppliedPrices> _fastPriceType;
	private readonly StrategyParam<AppliedPrices> _slowPriceType;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _trailingStep;
	private readonly StrategyParam<decimal> _minCrossDistance;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<bool> _confirmedOnEntry;
	private readonly StrategyParam<bool> _oneEntryPerBar;
	private readonly StrategyParam<bool> _stopAndReverse;
	private readonly StrategyParam<bool> _pureSar;
	private readonly StrategyParam<bool> _useHourTrade;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<DataType> _candleType;

	private IIndicator _fastMa;
	private IIndicator _slowMa;

	private decimal? _fastPrev;
	private decimal? _fastPrevPrev;
	private decimal? _slowPrev;
	private decimal? _slowPrevPrev;

	private DateTimeOffset? _lastEntryBar;
	private TradeDirections _lastTrade = TradeDirections.None;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Fast moving average method.
	/// </summary>
	public MovingAverageMethods FastMaType
	{
		get => _fastMaType.Value;
		set => _fastMaType.Value = value;
	}

	/// <summary>
	/// Slow moving average method.
	/// </summary>
	public MovingAverageMethods SlowMaType
	{
		get => _slowMaType.Value;
		set => _slowMaType.Value = value;
	}

	/// <summary>
	/// Price type used for the fast moving average.
	/// </summary>
	public AppliedPrices FastPriceType
	{
		get => _fastPriceType.Value;
		set => _fastPriceType.Value = value;
	}

	/// <summary>
	/// Price type used for the slow moving average.
	/// </summary>
	public AppliedPrices SlowPriceType
	{
		get => _slowPriceType.Value;
		set => _slowPriceType.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price units.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Additional move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStep
	{
		get => _trailingStep.Value;
		set => _trailingStep.Value = value;
	}

	/// <summary>
	/// Minimum distance between the averages to validate a crossover.
	/// </summary>
	public decimal MinCrossDistance
	{
		get => _minCrossDistance.Value;
		set => _minCrossDistance.Value = value;
	}

	/// <summary>
	/// Reverse buy and sell conditions.
	/// </summary>
	public bool ReverseCondition
	{
		get => _reverseCondition.Value;
		set => _reverseCondition.Value = value;
	}

	/// <summary>
	/// Confirm signals on closed candles only.
	/// </summary>
	public bool ConfirmedOnEntry
	{
		get => _confirmedOnEntry.Value;
		set => _confirmedOnEntry.Value = value;
	}

	/// <summary>
	/// Limit the strategy to a single entry per bar.
	/// </summary>
	public bool OneEntryPerBar
	{
		get => _oneEntryPerBar.Value;
		set => _oneEntryPerBar.Value = value;
	}

	/// <summary>
	/// Close the current trade and reverse when an opposite signal appears.
	/// </summary>
	public bool StopAndReverse
	{
		get => _stopAndReverse.Value;
		set => _stopAndReverse.Value = value;
	}

	/// <summary>
	/// Disable protective orders and rely purely on signal reversals.
	/// </summary>
	public bool PureSar
	{
		get => _pureSar.Value;
		set => _pureSar.Value = value;
	}

	/// <summary>
	/// Enable trading only within the selected hours.
	/// </summary>
	public bool UseHourTrade
	{
		get => _useHourTrade.Value;
		set => _useHourTrade.Value = value;
	}

	/// <summary>
	/// Hour when trading can start (0-23).
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when trading must end (0-23).
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}


	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="UniversalMaCrossStrategy"/>.
	/// </summary>
	public UniversalMaCrossStrategy()
	{
		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Fast moving average length", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Slow moving average length", "Indicators")
			
			.SetOptimize(30, 200, 5);

		_fastMaType = Param(nameof(FastMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Fast MA Type", "Method for fast average", "Indicators");

		_slowMaType = Param(nameof(SlowMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Slow MA Type", "Method for slow average", "Indicators");

		_fastPriceType = Param(nameof(FastPriceType), AppliedPrices.Close)
			.SetDisplay("Fast Price Type", "Price source for fast MA", "Indicators");

		_slowPriceType = Param(nameof(SlowPriceType), AppliedPrices.Close)
			.SetDisplay("Slow Price Type", "Price source for slow MA", "Indicators");

		_stopLoss = Param(nameof(StopLoss), 0m)
			.SetDisplay("Stop Loss", "Stop-loss distance in price", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 0m)
			.SetDisplay("Take Profit", "Take-profit distance in price", "Risk");

		_trailingStop = Param(nameof(TrailingStop), 0m)
			.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk");

		_trailingStep = Param(nameof(TrailingStep), 0m)
			.SetDisplay("Trailing Step", "Additional move before trailing", "Risk");

		_minCrossDistance = Param(nameof(MinCrossDistance), 0m)
			.SetDisplay("Min Cross Distance", "Minimum distance between averages", "Filters");

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Signals", "Swap long and short conditions", "General");

		_confirmedOnEntry = Param(nameof(ConfirmedOnEntry), true)
			.SetDisplay("Confirmed On Entry", "Use closed candles for signals", "General");

		_oneEntryPerBar = Param(nameof(OneEntryPerBar), true)
			.SetDisplay("One Entry Per Bar", "Allow only one entry per candle", "General");

		_stopAndReverse = Param(nameof(StopAndReverse), true)
			.SetDisplay("Stop And Reverse", "Close and reverse on opposite signal", "Risk");

		_pureSar = Param(nameof(PureSar), false)
			.SetDisplay("Pure SAR", "Disable stop-loss, take-profit and trailing", "Risk");

		_useHourTrade = Param(nameof(UseHourTrade), false)
			.SetDisplay("Use Hour Filter", "Limit trading by session hours", "Session");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Trading window start hour", "Session");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Trading window end hour", "Session");


		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle subscription", "General");
	}

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

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

		_fastMa = null;
		_slowMa = null;
		_fastPrev = null;
		_fastPrevPrev = null;
		_slowPrev = null;
		_slowPrevPrev = null;
		_lastEntryBar = null;
		_lastTrade = TradeDirections.None;
		ResetProtection();
	}

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

		_fastMa = CreateMovingAverage(FastMaType, FastMaPeriod);
		_slowMa = CreateMovingAverage(SlowMaType, SlowMaPeriod);

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}

	}

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

		ManageExistingPosition(candle);

		if (_fastMa is null || _slowMa is null)
			return;

		var fastPrice = GetPrice(candle, FastPriceType);
		var slowPrice = GetPrice(candle, SlowPriceType);

		var fastResult = _fastMa.Process(new DecimalIndicatorValue(_fastMa, fastPrice, candle.OpenTime) { IsFinal = true });
		var slowResult = _slowMa.Process(new DecimalIndicatorValue(_slowMa, slowPrice, candle.OpenTime) { IsFinal = true });

		if (fastResult.IsEmpty || slowResult.IsEmpty)
			return;

		var fastValue = fastResult.ToDecimal();
		var slowValue = slowResult.ToDecimal();

		var prevFast = _fastPrev;
		var prevSlow = _slowPrev;
		var prevFastPrev = _fastPrevPrev;
		var prevSlowPrev = _slowPrevPrev;

		_fastPrevPrev = prevFast;
		_slowPrevPrev = prevSlow;
		_fastPrev = fastValue;
		_slowPrev = slowValue;


		bool crossUp = false;
		bool crossDown = false;

		if (ConfirmedOnEntry)
		{
			if (prevFast.HasValue && prevSlow.HasValue && prevFastPrev.HasValue && prevSlowPrev.HasValue)
			{
				var fastPrevPrevValue = prevFastPrev.Value;
				var slowPrevPrevValue = prevSlowPrev.Value;
				var fastPrevValue = prevFast.Value;
				var slowPrevValue = prevSlow.Value;
				var diff = fastPrevValue - slowPrevValue;

				crossUp = fastPrevPrevValue < slowPrevPrevValue && fastPrevValue > slowPrevValue && diff >= MinCrossDistance;
				crossDown = fastPrevPrevValue > slowPrevPrevValue && fastPrevValue < slowPrevValue && -diff >= MinCrossDistance;
			}
		}
		else
		{
			if (prevFast.HasValue && prevSlow.HasValue)
			{
				var fastPrevValue = prevFast.Value;
				var slowPrevValue = prevSlow.Value;
				var diff = fastValue - slowValue;

				crossUp = fastPrevValue < slowPrevValue && fastValue > slowValue && diff >= MinCrossDistance;
				crossDown = fastPrevValue > slowPrevValue && fastValue < slowValue && -diff >= MinCrossDistance;
			}
		}

		bool buySignal;
		bool sellSignal;

		if (!ReverseCondition)
		{
			buySignal = crossUp;
			sellSignal = crossDown;
		}
		else
		{
			buySignal = crossDown;
			sellSignal = crossUp;
		}

		var canTrade = IsWithinTradingHours(candle);

		if (!canTrade)
			return;

		if (StopAndReverse && Position != 0)
		{
			if ((_lastTrade == TradeDirections.Long && sellSignal) || (_lastTrade == TradeDirections.Short && buySignal))
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}

		if (Position != 0)
			return;

		var entryAllowed = !OneEntryPerBar || _lastEntryBar != candle.OpenTime;

		if (!entryAllowed)
			return;

		if (buySignal)
		{
			BuyMarket(Volume);
			SetProtectionLevels(candle.ClosePrice, true);
			_lastTrade = TradeDirections.Long;
			_lastEntryBar = candle.OpenTime;
		}
		else if (sellSignal)
		{
			SellMarket(Volume);
			SetProtectionLevels(candle.ClosePrice, false);
			_lastTrade = TradeDirections.Short;
			_lastEntryBar = candle.OpenTime;
		}
	}

	private void ManageExistingPosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetProtection();
			return;
		}

		UpdateTrailingStop(candle);

		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (PureSar || TrailingStop <= 0m || !_entryPrice.HasValue)
			return;

		var activationDistance = TrailingStop + TrailingStep;

		if (Position > 0)
		{
			if (candle.ClosePrice - _entryPrice.Value > activationDistance)
			{
				var activationLevel = candle.ClosePrice - activationDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value < activationLevel)
				{
					var newStop = candle.ClosePrice - TrailingStop;
					_stopPrice = _stopPrice.HasValue ? Math.Max(_stopPrice.Value, newStop) : newStop;
				}
			}
		}
		else if (Position < 0)
		{
			if (_entryPrice.Value - candle.ClosePrice > activationDistance)
			{
				var activationLevel = candle.ClosePrice + activationDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value > activationLevel)
				{
					var newStop = candle.ClosePrice + TrailingStop;
					_stopPrice = _stopPrice.HasValue ? Math.Min(_stopPrice.Value, newStop) : newStop;
				}
			}
		}
	}

	private bool IsWithinTradingHours(ICandleMessage candle)
	{
		if (!UseHourTrade)
			return true;

		var hour = candle.OpenTime.Hour;
		var start = StartHour;
		var end = EndHour;

		if (start <= end)
			return hour >= start && hour <= end;

		return hour >= start || hour <= end;
	}

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

	private static decimal GetPrice(ICandleMessage candle, AppliedPrices priceType)
	{
		return priceType 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 + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}

	private void SetProtectionLevels(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;

		if (PureSar)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		var stop = StopLoss;
		var take = TakeProfit;

		_stopPrice = stop > 0m ? (isLong ? entryPrice - stop : entryPrice + stop) : null;
		_takeProfitPrice = take > 0m ? (isLong ? entryPrice + take : entryPrice - take) : null;
	}

	private void ResetProtection()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private enum TradeDirections
	{
		None,
		Long,
		Short
	}
}