GitHub で見る

連続損失戦略で取引を一時停止する

連続損失時の取引一時停止戦略は、MetaTrader 4 エキスパート アドバイザー 「連続損失時の取引一時停止」 のリスク制御ロジックを再現します。元のスクリプトは、最近終了した取引を監視し、マイナスの利益で終了した取引の数を数え、連敗が短期間内にユーザー定義の制限を超えた場合、新規注文を一時停止しました。 StockSharp ポートは、その動作を維持しながら最小運動量エントリ モデルをラップするため、スタンドアロン ストラテジー内で一時停止メカニズムを評価できます。

仕組み

  1. この戦略は、CandleType で指定されたタイムフレーム ローソク足をサブスクライブします。完成したローソクが到着するたびに、終値が前の終値と比較されます。それが増加した場合、戦略はロングエントリーを試みます。減少した場合はショートエントリーが検討されます。強気のポジションが弱気のローソク足に面している場合(終値が始値を下回っている場合)、または弱気ポジションが強気のローソク足に面している場合(終値が始値を上回っている場合)、ポジションは終了します。
  2. クローズされたポジションごとに、ストラテジーの実現利益が検査されます。負けた結果は、連続した負けのみを保存する内部 FIFO リストの終了タイムスタンプをキューに入れます。 MQL ループが損失のない取引に遭遇すると中止されるのと同じように、利益のある出口や損益分岐点の出口はリストを消去します。
  3. リストが ConsecutiveLosses 個の項目に達すると、戦略は最も古い損失と最も新しい損失の間の時間差が WithinMinutes 以内であるかどうかをチェックします。そうである場合、取引は最後の終了時刻から PauseMinutes が経過するまで一時停止されます。一時停止中は新しい成行注文は発行されませんが、既存のポジション管理は継続して行われるため、ブックは自然に平坦化することができます。
  4. 一時停止が終了すると、損失リストがクリアされ、取引が自動的に再開されます。この動作は、永続的な注文履歴スキャンに依存せずに、元の CheckLastNLossDifference および lastOrderCloseTime 関数を模倣します。

この実装では、StockSharp の高レベルのキャンドル サブスクリプション (SubscribeCandles) と組み込みの損益マネージャーを使用して実現利益を監視します。シンプルなキュー (Queue<DateTimeOffset>) は、冗長な手動履歴走査の禁止を尊重しながら、連敗記録のタイムスタンプをキャプチャします。

パラメーター

パラメータ デフォルト 説明
CandleType 5分の時間枠 単純なモメンタムエントリに使用されるローソク足の集計。
OrderVolume 0.1 各エントリー注文とエグジット注文とともに送信されるボリューム (ロット/契約)。
ConsecutiveLosses 3 新しい取引が一時停止されるまでに必要な連続負けポジションの数。
WithinMinutes 20 連敗の最初と最後の損失の間に許容される最大分数。値をゼロにすると、ウィンドウ チェックが無効になります。
PauseMinutes 20 連敗が検出された後の取引停止期間。

注意事項

  • 損失タイムスタンプのキューは、戦略がフラットで損失を認識したばかりの場合にのみ設定されます。部分的な決済や収益性の高い取引は連続記録を延長せず、誤検知を防ぎます。
  • 一時停止タイマーは、終了した各キャンドルに対して評価されます。ストラテジーがアイドル状態の間に PauseMinutes が経過すると、次のローソク足がすぐに取引のロックを解除します。
  • StockSharp バージョンはネッティング ポジションで動作するため、実現された損益の差は PnLManager.RealizedPnL から導出され、注文ログ全体を再処理することなく、MetaTrader 履歴ルックアップを忠実に反映します。
using System;
using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "Pause Trading On Consecutive Loss" MetaTrader expert.
/// Uses simple momentum entries (close vs previous close) with a pause mechanism
/// that halts trading after consecutive losing trades within a time window.
/// </summary>
public class PauseTradingOnConsecutiveLossStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _consecutiveLosses;
	private readonly StrategyParam<int> _pauseBars;

	private decimal? _previousClose;
	private int _lossStreak;
	private int _pauseCountdown;
	private decimal _entryPrice;
	private Sides? _entryDirection;

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

	public int ConsecutiveLosses
	{
		get => _consecutiveLosses.Value;
		set => _consecutiveLosses.Value = value;
	}

	public int PauseBars
	{
		get => _pauseBars.Value;
		set => _pauseBars.Value = value;
	}

	public PauseTradingOnConsecutiveLossStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for momentum entries", "General");

		_consecutiveLosses = Param(nameof(ConsecutiveLosses), 3)
			.SetGreaterThanZero()
			.SetDisplay("Consecutive Losses", "Losses before pausing", "Risk");

		_pauseBars = Param(nameof(PauseBars), 8)
			.SetGreaterThanZero()
			.SetDisplay("Pause Bars", "Number of bars to pause after loss streak", "Risk");
	}

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

		_previousClose = null;
		_lossStreak = 0;
		_pauseCountdown = 0;
		_entryPrice = 0;
		_entryDirection = null;

		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;

		var close = candle.ClosePrice;

		if (_previousClose is null)
		{
			_previousClose = close;
			return;
		}

		var volume = Volume;
		if (volume <= 0)
			volume = 1;
		var momentumThreshold = _previousClose.Value * 0.003m;

		// Check if we should pause
		if (_pauseCountdown > 0)
		{
			_pauseCountdown--;
			_previousClose = close;
			return;
		}

		// Check for exit and track wins/losses
		if (Position != 0)
		{
			var shouldExit = false;

			if (Position > 0 && close < _previousClose.Value - momentumThreshold)
				shouldExit = true;
			else if (Position < 0 && close > _previousClose.Value + momentumThreshold)
				shouldExit = true;

			if (shouldExit)
			{
				// Determine if this was a winning or losing trade
				var isLoss = false;
				if (_entryDirection == Sides.Buy && close < _entryPrice)
					isLoss = true;
				else if (_entryDirection == Sides.Sell && close > _entryPrice)
					isLoss = true;

				if (isLoss)
				{
					_lossStreak++;
					if (_lossStreak >= ConsecutiveLosses)
					{
						_pauseCountdown = PauseBars;
						_lossStreak = 0;
					}
				}
				else
				{
					_lossStreak = 0;
				}

				// Close position
				if (Position > 0)
					SellMarket(Position);
				else if (Position < 0)
					BuyMarket(Math.Abs(Position));

				_entryDirection = null;
			}
		}

		// New entry: momentum - close > prev close -> buy, close < prev close -> sell
		if (Position == 0 && _entryDirection is null)
		{
			if (close > _previousClose.Value + momentumThreshold)
			{
				BuyMarket(volume);
				_entryPrice = close;
				_entryDirection = Sides.Buy;
			}
			else if (close < _previousClose.Value - momentumThreshold)
			{
				SellMarket(volume);
				_entryPrice = close;
				_entryDirection = Sides.Sell;
			}
		}

		_previousClose = close;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_previousClose = null;
		_lossStreak = 0;
		_pauseCountdown = 0;
		_entryPrice = 0;
		_entryDirection = null;

		base.OnReseted();
	}
}