在 GitHub 上查看

连续亏损暂停交易策略

连续亏损暂停交易策略 复刻了 MetaTrader 4 智能交易系统 “Pause Trading On Consecutive Loss” 的风控思想。原始脚本会检查最近的平仓订单,统计其中连续亏损的笔数,并在亏损次数在限定时间窗口内达到阈值时暂停新的下单。StockSharp 版本沿用了这一逻辑,并嵌入了一个最小化的动量入场模型,使得暂停功能可以在独立策略中被回测和验证。

工作流程

  1. 策略按照 CandleType 指定的周期订阅K线。当收到一根完成的K线后,将收盘价与上一根的收盘价比较。若收盘价上升则尝试做多,下降则考虑做空。若持有多头且出现阴线(收盘价低于开盘价),或持有空头且出现阳线(收盘价高于开盘价),仓位会被平掉。
  2. 每次仓位归零后都会检查策略的已实现盈亏。若出现亏损,平仓时间会被加入一个仅保存连续亏损记录的 FIFO 队列;一旦出现盈利或保本的平仓,队列立即清空,正如 MQL 实现遇到非亏损订单时会终止遍历。
  3. 当队列长度达到 ConsecutiveLosses 时,策略会判断最早和最新亏损之间的时间差是否小于 WithinMinutes。若条件成立,则从最近一次平仓时间起暂停交易 PauseMinutes 分钟。在暂停期间不会提交新的市价单,但已有仓位仍可按照既定规则出场,以便仓位自然归零。
  4. 暂停时间结束后,亏损队列被重置,交易自动恢复。该行为与原始 CheckLastNLossDifferencelastOrderCloseTime 函数保持一致,而无需重复扫描整段订单历史。

实现过程中使用了 StockSharp 的高级 K 线订阅接口(SubscribeCandles)和内置盈亏管理器来跟踪已实现盈亏。一个简单的 Queue<DateTimeOffset> 用于保存亏损时间戳,避免手工遍历冗余的历史数据。

参数说明

参数 默认值 说明
CandleType 5 分钟K线 用于简单动量判断的K线聚合类型。
OrderVolume 0.1 每次进出场的下单数量(手数/合约数)。
ConsecutiveLosses 3 触发暂停所需的连续亏损次数。
WithinMinutes 20 允许亏损序列发生的最长分钟数,设置为 0 则关闭时间窗口限制。
PauseMinutes 20 连续亏损后暂停交易的时长(分钟)。

额外说明

  • 只有在仓位刚刚归零且出现亏损时,亏损队列才会记录时间戳。部分平仓或盈利出场不会增加序列,能够避免误判。
  • 暂停计时在每根完成的K线中检查;如果 PauseMinutes 期间没有信号,下一个K线到来时会立即恢复交易。
  • 由于 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();
	}
}