GitHub で見る

トレーリングストップマネージャー戦略

この戦略はMetaTraderエキスパートMQL/17263/TrailingStop.mq5のトレーリングストップコントローラーを再現します。エントリーが既に開かれた後のストップロス管理を自動化することに焦点を当てています。

オリジナルのアイデア

  • 出典: ヘッジアカウント向けのVladimir KarputovのTrailingStopエキスパート。
  • コンセプト: 最初のティックでEAはロングとショートの両ポジションを開き、その後pipベースの距離を使用して各サイドのストップロスレベルを独立して引き上げました。
  • 目的: 設定可能な有効化距離と更新ステップでストップをトレーリングする方法を示す。

StockSharpへの適応

  • ネッティング互換性: StockSharpの戦略はネットポジションで動作するため、このポートは一度に一方向を管理します。両サイドを同時にトレーリングするには、2つの戦略インスタンスを起動してください。
  • ティックベースの更新: 戦略はトレードティック(DataType.Ticks)を購読して、MetaTraderのティック駆動の調整を反映します。
  • pip変換: 設定されたpip値にSecurity.PriceStepを掛け(取引所がステップを提供しない場合は1にフォールバック)、入力を絶対価格オフセットに変換します。
  • オプションの自動エントリー: パラメーターを使用して起動時に即時成行注文を送ることができ、クイックデモンストレーションや手動テストに便利です。

トレーディングロジック

  1. 起動
    • 銘柄の価格ステップを読み取り、ティックデータを購読します。
    • Initial Directionパラメーターに従って、オプションで成行注文を送信します。
  2. エントリー追跡
    • 各自己トレードがトレーリング状態をリセットし、実際の執行価格を新しい参照として保存します。
  3. 有効化
    • ロングポジションの場合、トレーリングエンジンはエントリーから価格がTrailing Stop (pips)だけ進んだ後にのみ有効化されます。ショートの場合は同等の下落が必要です。
  4. ストップ調整
    • 有効化されると、ストップレベルは現在のティック価格から有効化距離を引いた/加えた値になります。
    • ストップは最新のティックが少なくともTrailing Step (pips)前に押し進めた場合にのみ移動されます。
    • ゼロステップは、ストップが有利なティックごとに更新されることを意味します。
  5. 決済
    • 価格がトレーリングレベルに戻るかそれを超えると、戦略は成行注文を使用して残りのポジションをクローズします。

パラメーター

名前 説明
Trailing Stop (pips) pipsでの有効化距離。ゼロより大きくする必要があります。
Trailing Step (pips) ストップを再度進める前の最小有利移動(pips単位)。ゼロでも可。
Initial Direction OnStarted中に配置されるオプションの成行注文(NoneLongShort)。

追加メモ

  • オリジナルのエキスパートはbid/ask値を使用していました。このC#バージョンは最後のトレード価格を近似値として使用しており、ほとんどの流動性の高い銘柄には十分です。
  • テイクプロフィットや新規エントリーロジックは含まれていません。このコンポーネントを別のシグナル戦略と組み合わせるか、ポジションを手動で開いた後に起動することができます。
  • ブローカーが分数pipステップを提供する場合、Security.PriceStepがそれを反映していることを確認してください。そうでない場合は、実際のティックサイズに合わせてpip値を調整してください。
  • このモジュールには自動テストがないため、実際の資金を展開する前にデモフィードで検証してください。
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>
/// Trailing stop manager that mirrors the pip-based trailing logic from the original MQL expert.
/// </summary>
public class TrailingStopManagerStrategy : Strategy
{
	/// <summary>
	/// Direction of the optional market order placed when the strategy starts.
	/// </summary>
	public enum InitialDirections
	{
		None,
		Long,
		Short
	}

	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<InitialDirections> _startDirection;

	private decimal _entryPrice;
	private decimal _trailingStopPrice;
	private bool _trailingActive;
	private InitialDirections _currentDirection = InitialDirections.None;
	private decimal _priceStep = 1m;

	/// <summary>
	/// Trailing stop activation distance expressed in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum pip distance that must be covered before the trailing stop is adjusted again.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Optional market order direction executed on start to quickly demonstrate trailing behaviour.
	/// </summary>
	public InitialDirections StartDirection
	{
		get => _startDirection.Value;
		set => _startDirection.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="TrailingStopManagerStrategy"/> class.
	/// </summary>
	public TrailingStopManagerStrategy()
	{
		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Stop (pips)", "Distance to activate trailing", "Risk Management")
			;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Minimal move before adjusting stop", "Risk Management")
			;

		_startDirection = Param(nameof(StartDirection), InitialDirections.None)
			.SetDisplay("Initial Direction", "Optional market order on start", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "Data");
	}

	private readonly StrategyParam<DataType> _candleType;

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

	private readonly List<decimal> _closes = new();
	private const int FastLen = 10;
	private const int SlowLen = 30;
	private decimal? _prevFast;
	private decimal? _prevSlow;

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

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

		// Retrieve price step to convert pip values into price offsets.
		_priceStep = Security?.PriceStep ?? 1m;
		if (_priceStep <= 0m)
			_priceStep = 1m;

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

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

		_entryPrice = 0m;
		_trailingStopPrice = 0m;
		_trailingActive = false;
		_currentDirection = InitialDirections.None;
		_priceStep = 1m;
		_closes.Clear();
		_prevFast = null;
		_prevSlow = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Trade == null)
			return;

		var tradePrice = trade.Trade.Price;

		// Reset trailing state whenever a new position is opened.
		if (Position > 0 && trade.Order.Side == Sides.Buy)
		{
			_entryPrice = tradePrice;
			_trailingActive = false;
			_trailingStopPrice = 0m;
			_currentDirection = InitialDirections.Long;
		}
		else if (Position < 0 && trade.Order.Side == Sides.Sell)
		{
			_entryPrice = tradePrice;
			_trailingActive = false;
			_trailingStopPrice = 0m;
			_currentDirection = InitialDirections.Short;
		}
		else if (Position == 0)
		{
			ResetTrailing();
		}
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0)
			ResetTrailing();
	}

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

		_closes.Add(candle.ClosePrice);
		if (_closes.Count > SlowLen + 10) _closes.RemoveAt(0);

		if (_closes.Count < SlowLen)
			return;

		var fast = _closes.Skip(_closes.Count - FastLen).Take(FastLen).Average();
		var slow = _closes.Skip(_closes.Count - SlowLen).Take(SlowLen).Average();

		var prevFast = _prevFast;
		var prevSlow = _prevSlow;
		_prevFast = fast;
		_prevSlow = slow;

		// Entry logic: use SMA crossover when flat
		if (Position == 0)
		{
			if (prevFast is decimal lastFast && prevSlow is decimal lastSlow && lastFast <= lastSlow && fast > slow)
			{
				BuyMarket();
				_entryPrice = candle.ClosePrice;
				_trailingActive = false;
				_trailingStopPrice = 0m;
				_currentDirection = InitialDirections.Long;
			}
			else if (prevFast is decimal lastFast2 && prevSlow is decimal lastSlow2 && lastFast2 >= lastSlow2 && fast < slow)
			{
				SellMarket();
				_entryPrice = candle.ClosePrice;
				_trailingActive = false;
				_trailingStopPrice = 0m;
				_currentDirection = InitialDirections.Short;
			}
			return;
		}

		var price = candle.ClosePrice;

		if (Position > 0 && _currentDirection == InitialDirections.Long)
		{
			UpdateLongTrailing(price);
		}
		else if (Position < 0 && _currentDirection == InitialDirections.Short)
		{
			UpdateShortTrailing(price);
		}
	}

	private void UpdateLongTrailing(decimal price)
	{
		var stopDistance = TrailingStopPips * _priceStep;
		if (stopDistance <= 0m)
			return;

		var stepDistance = TrailingStepPips * _priceStep;

		// Activate the trailing stop once price has moved far enough above the entry.
		if (!_trailingActive)
		{
			if (price - _entryPrice >= stopDistance)
			{
				_trailingActive = true;
				_trailingStopPrice = _entryPrice;
			}
		}
		else
		{
			var desiredStop = price - stopDistance;

			// Only move the stop forward when the configured step distance is met.
			if (stepDistance <= 0m)
			{
				if (desiredStop > _trailingStopPrice)
					_trailingStopPrice = desiredStop;
			}
			else if (desiredStop - _trailingStopPrice >= stepDistance)
			{
				_trailingStopPrice = desiredStop;
			}
		}

		// Exit once price drops to the trailing stop level.
		if (_trailingActive && price <= _trailingStopPrice)
			ExitLong();
	}

	private void UpdateShortTrailing(decimal price)
	{
		var stopDistance = TrailingStopPips * _priceStep;
		if (stopDistance <= 0m)
			return;

		var stepDistance = TrailingStepPips * _priceStep;

		// Activate the trailing stop once price has moved far enough below the entry.
		if (!_trailingActive)
		{
			if (_entryPrice - price >= stopDistance)
			{
				_trailingActive = true;
				_trailingStopPrice = _entryPrice;
			}
		}
		else
		{
			var desiredStop = price + stopDistance;

			// Only move the stop forward when the configured step distance is met.
			if (stepDistance <= 0m)
			{
				if (desiredStop < _trailingStopPrice || _trailingStopPrice == 0m)
					_trailingStopPrice = desiredStop;
			}
			else if (_trailingStopPrice - desiredStop >= stepDistance)
			{
				_trailingStopPrice = desiredStop;
			}
		}

		// Exit once price rises to the trailing stop level.
		if (_trailingActive && price >= _trailingStopPrice)
			ExitShort();
	}

	private void ExitLong()
	{
		if (Position <= 0)
			return;

		SellMarket();
	}

	private void ExitShort()
	{
		if (Position >= 0)
			return;

		BuyMarket();
	}

	private void ResetTrailing()
	{
		_entryPrice = 0m;
		_trailingStopPrice = 0m;
		_trailingActive = false;
		_currentDirection = InitialDirections.None;
	}
}