GitHub で見る

EMAクロスオーバー・トレーリング戦略

概要

この戦略はMQL5エキスパートアドバイザー "Intersection 2 iMA" のStockSharpポートです。2本の指数移動平均(EMA)で動作し、完全に形成されたローソク足で発生するクロスオーバーに反応します。元のエキスパートはMetaTrader 5向けに設計され、トレードボリュームを動的に管理していました;この変換では注文サイズは設定可能なパラメーターで制御し、クロスオーバーとトレーリングのロジックを維持します。

トレードロジック

  1. シグナル生成
    • 選択されたローソク足シリーズで高速・低速EMAを計算する。
    • 強気クロスオーバー(高速EMAが低速EMAを上方クロス)は、前のローソク足が高速EMAが低速EMAと同等以下で閉じ、現在の値が高速EMAを低速EMAより上に示すときに買いシグナルを発動する。
    • 弱気クロスオーバー(高速EMAが低速EMAを下方クロス)は上記のルールを反映し、売りシグナルを生成する。
  2. 注文実行
    • 買いシグナルが発生しロングポジションが存在しない場合、戦略は成行買い注文を送信する。
    • 売りシグナルが発生しショートポジションが存在しない場合、戦略は成行売り注文を送信する。
    • 反対ポジションがある場合、まず既存ポジションを閉じてから新規ポジションを建てるために注文ボリュームが増加される(最初に反対トレードを閉じていたソースEAの動作に一致)。
  3. トレーリングストップ管理
    • 段階的なトレーリングストップが最も有利な価格から固定距離(価格刻みで)を維持する。
    • ストップはユーザー定義のステップ分価格が前進した場合にのみ移動し、注文の常時変更を防ぐ。
    • 価格がトレーリングレベルを違反した場合、ポジションは成行注文で閉じられる。

パラメーター

名前 説明 デフォルト値
FastPeriod 高速EMAの長さ。 4
SlowPeriod 低速EMAの長さ。 18
TrailingStopPoints 市場価格とトレーリングストップ間の距離(価格刻み/ポイント)。0はトレーリングを無効にする。 20
TrailingStepPoints トレーリングストップが前進する前の最小進捗(価格刻み)。 5
CandleType 計算に使用するローソク足データシリーズ(時間軸)。 15分足
TradeVolume 成行エントリーの注文サイズ。 1

実装上の注意

  • 戦略は高レベルAPI SubscribeCandles().Bind(...)を使用してローソク足データをEMAインジケーターと接続し、手動のバッファ管理が不要であることを保証します。
  • トレーリング距離は設定されたポイント数に銘柄のPriceStepを掛けて計算され、MQLバージョンの桁数調整ロジックを再現します。
  • トレーリングストップはMetaTraderで使用されるのと同じPositionModifyヘルパーをStockSharpが公開していないため、内部的に成行出口を使用して実装されます。動作は同等のままです:トレーリングレベルが違反されるとポジションは即座に終了します。
  • パラメーターはデザイナーで最適化するかUIから調整できるようにStrategyParam<T>を通じて公開されます。

使用上のヒント

  • インジケーター値を一貫して保つため、バックテストやライブトレードで使用する時間軸に合わせてCandleTypeを調整する。
  • ティックサイズが小さい銘柄をトレードする際はTrailingStopPointsTrailingStepPointsを相応に調整する;実効的な価格距離はポイント × PriceStepに等しい。
  • 希望するコントラクトまたはロットサイズに一致するようTradeVolumeを設定する。戦略は新しいシグナルが現れると反対ポジションを閉じるために注文金額を自動的に増加させる。

元のエキスパートアドバイザーとの違い

  • MetaTraderのマネー管理はMoneyFixedMarginを使用していました;StockSharpバージョンは代わりに固定注文ボリュームパラメーターを公開し、高度なポジションサイジングは外部設定に委ねます。
  • EAは未使用のInpCloseHalf入力を提供していました。ソースコードには効果がなく、省略されました。
  • ストップトレーリングはStockSharp内での実行を簡素化し、出口ロジックを同一に保つため、ストップロス注文を変更するのではなく内部的に処理されます。
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>
/// EMA crossover strategy with trailing stop logic converted from the MQL5 "Intersection 2 iMA" expert advisor.
/// The strategy opens trades on moving average crossovers and maintains a stepped trailing stop.
/// </summary>
public class EmaCrossoverTrailingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private ExponentialMovingAverage _fastEma = null!;
	private ExponentialMovingAverage _slowEma = null!;

	private decimal? _previousFastValue;
	private decimal? _previousSlowValue;

	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;

	private decimal _stopDistance;
	private decimal _stepDistance;

	/// <summary>
	/// Initializes <see cref="EmaCrossoverTrailingStrategy"/>.
	/// </summary>
	public EmaCrossoverTrailingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 4)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fast exponential moving average", "Moving Averages")
			
			.SetOptimize(2, 20, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 18)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slow exponential moving average", "Moving Averages")
			
			.SetOptimize(10, 60, 2);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Distance from price to trailing stop expressed in price steps", "Risk Management")
			
			.SetOptimize(5m, 40m, 5m);

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Minimum price advancement before the trailing stop is moved", "Risk Management")
			
			.SetOptimize(1m, 10m, 1m);

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

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume used for entries", "General");
	}

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Minimum move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Volume used for market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

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

		Volume = TradeVolume;
		_previousFastValue = null;
		_previousSlowValue = null;
		_longStopPrice = null;
		_shortStopPrice = null;
		_fastEma = null!;
		_slowEma = null!;
		_stopDistance = 0m;
		_stepDistance = 0m;
	}

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

		Volume = TradeVolume;

		_fastEma = new EMA { Length = FastPeriod };
		_slowEma = new EMA { Length = SlowPeriod };

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

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

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

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

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

		UpdateTrailingStops(candle);

		if (!_fastEma.IsFormed || !_slowEma.IsFormed)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		// indicators bound via .Bind()

		if (_previousFastValue is null || _previousSlowValue is null)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		var fastPrev = _previousFastValue.Value;
		var slowPrev = _previousSlowValue.Value;

		var crossedUp = fastPrev <= slowPrev && fastValue > slowValue;
		var crossedDown = fastPrev >= slowPrev && fastValue < slowValue;

		if (crossedUp && Position <= 0)
		{
			var volumeToBuy = TradeVolume;

			if (Position < 0)
				volumeToBuy += Math.Abs(Position);

			if (volumeToBuy > 0)
			{
				BuyMarket();
				InitializeLongTrailing(candle.ClosePrice);
			}
		}
		else if (crossedDown && Position >= 0)
		{
			var volumeToSell = TradeVolume;

			if (Position > 0)
				volumeToSell += Math.Abs(Position);

			if (volumeToSell > 0)
			{
				SellMarket();
				InitializeShortTrailing(candle.ClosePrice);
			}
		}

		_previousFastValue = fastValue;
		_previousSlowValue = slowValue;
	}

	private decimal CalculateDistance(decimal points)
	{
		if (points <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m)
			priceStep = 1m;

		return points * priceStep;
	}

	private void InitializeLongTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			return;
		}

		_longStopPrice = price - _stopDistance;
		_shortStopPrice = null;
	}

	private void InitializeShortTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_shortStopPrice = null;
			return;
		}

		_shortStopPrice = price + _stopDistance;
		_longStopPrice = null;
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			_shortStopPrice = null;
			return;
		}

		if (Position > 0)
		{
			if (_longStopPrice is null)
			{
				InitializeLongTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice - _stopDistance;

				if (newStop - _longStopPrice.Value >= _stepDistance)
					_longStopPrice = newStop;

				if (candle.LowPrice <= _longStopPrice.Value)
				{
					SellMarket();
					_longStopPrice = null;
				}
			}
		}
		else if (Position < 0)
		{
			if (_shortStopPrice is null)
			{
				InitializeShortTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice + _stopDistance;

				if (_shortStopPrice.Value - newStop >= _stepDistance)
					_shortStopPrice = newStop;

				if (candle.HighPrice >= _shortStopPrice.Value)
				{
					BuyMarket();
					_shortStopPrice = null;
				}
			}
		}
		else
		{
			_longStopPrice = null;
			_shortStopPrice = null;
		}
	}
}