在 GitHub 上查看

N Candles v6 策略

概述

N Candles v6 策略跟踪最近完成的蜡烛,寻找方向完全相同的连续序列。当市场连续出现 N 根阳线时,策略建立多头仓位;连续出现 N 根阴线时则开立空头。该方案源自 MetaTrader 专家顾问 N Candles v6.mq5,并针对 StockSharp 高级 API 重新实现。

该算法适用于任何提供常规时间周期蜡烛的品种。可以通过交易时间窗口控制进场时段,而已经建立的仓位在窗口关闭时仍然受到止损、止盈与追踪止损的保护。

交易逻辑

  1. 订阅设定的蜡烛类型,只处理状态为 Finished 的蜡烛。
  2. 统计连续的阳线(Close > Open)与阴线(Close < Open),十字线会重置计数。
  3. 当检测到 CandlesCount 根阳线:
    • 预测加仓后的净头寸不超过 MaxPositionVolume
    • 发送市价买单;若当前持有空头,会自动增加手数以一次性反向至多头。
  4. 当检测到 CandlesCount 根阴线:
    • 确认新的空头不会突破 MaxPositionVolume
    • 发送市价卖单;若当前持有多头,同样会扩大手数以完成反向。
  5. 若最新蜡烛破坏了连阳/连阴结构(“黑羊”):
    • 根据 ClosingMode 仅执行一次平仓操作:全部平仓、只平反向仓,或只平顺势仓。
  6. 每根蜡烛都会执行风控:
    • 止损与止盈按点数和 PriceStep 计算为绝对价差。
    • 价格在有利方向移动 TrailingStopPips + TrailingStepPips 后启动追踪止损,并且只沿盈利方向推进。
    • 一旦价格触及止损、止盈或追踪止损,立即平掉全部仓位。

风险控制

  • Stop Loss (pips):将点数转换为绝对价格距离;对于 5 位或 3 位小数报价,会自动放大 10 倍以符合传统“pip”定义。
  • Take Profit (pips):达到设定盈利点数后平仓,设置为 0 则关闭该功能。
  • Trailing Stop / Step (pips):启用追踪止损后,价格至少移动指定的点数才会更新止损位置;当 TrailingStopPips > 0 时,TrailingStepPips 必须大于 0。
  • Max Position Volume:限制净头寸的绝对值,超出限制的信号会被忽略。
  • Closing Mode:决定出现“黑羊”时的处理方式:
    • All – 平掉全部仓位。
    • Opposite – 仅平掉与连阳/连阴方向相反的仓位。
    • Unidirectional – 仅平掉与连阳/连阴方向相同的仓位。
  • 交易时间窗口:只有当蜡烛的开盘时间处于 StartHourEndHour(包含端点)之间时才允许开仓;保护性平仓逻辑始终有效。

参数

名称 默认值 说明
CandlesCount 3 触发信号所需的连续同向蜡烛数量。
OrderVolume 0.01 基础下单手数;若存在反向仓位,会附加相应数量以完成反向。
TakeProfitPips 50 止盈点数,0 为关闭。
StopLossPips 50 止损点数,0 为关闭。
TrailingStopPips 10 追踪止损的距离,0 为关闭。
TrailingStepPips 4 每次更新追踪止损所需的最小价格改进;启用追踪止损时必须 > 0。
MaxPositionVolume 2 净头寸的最大绝对值。
UseTradingHours true 是否启用交易时间过滤。
StartHour 11 允许开仓的起始小时(0-23)。
EndHour 18 允许开仓的结束小时(0-23)。
ClosingMode All 出现“黑羊”时的平仓策略。
CandleType 1 小时蜡烛 用于信号计算的数据类型。

其他说明

  • Pip 换算基于 PriceStep,对 5 位或 3 位小数报价会自动乘以 10。
  • 策略启动时调用 StartProtection(),以启用 StockSharp 提供的安全保护机制(异常断线、撤单等)。
  • 策略依据净头寸 (Strategy.Position) 运作,适合净额账户;通过提高 MaxPositionVolume 可近似实现多头/空头同时持仓的效果。
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>
/// Detects runs of identical candles and trades in the direction of the streak.
/// </summary>
public class NCandlesV6Strategy : Strategy
{
	/// <summary>
	/// Defines how positions are closed when a candle breaks the streak.
	/// </summary>
	public enum BlackSheepCloseModes
	{
		/// <summary>
		/// Close every open position regardless of direction.
		/// </summary>
		All,

		/// <summary>
		/// Close only positions that oppose the detected streak.
		/// </summary>
		Opposite,

		/// <summary>
		/// Close only positions that follow the detected streak.
		/// </summary>
		Unidirectional,
	}

	private readonly StrategyParam<int> _candlesCount;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _maxPositionVolume;
	private readonly StrategyParam<bool> _useTradingHours;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<BlackSheepCloseModes> _blackSheepMode;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal? _trailingLong;
	private decimal? _trailingShort;
	private int _streakDirection;
	private int _bullCount;
	private int _bearCount;
	private bool _blackSheepTriggered;

	public int CandlesCount { get => _candlesCount.Value; set => _candlesCount.Value = value; }
	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
	public decimal TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
	public decimal StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public decimal TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
	public decimal TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
	public decimal MaxPositionVolume { get => _maxPositionVolume.Value; set => _maxPositionVolume.Value = value; }
	public bool UseTradingHours { get => _useTradingHours.Value; set => _useTradingHours.Value = value; }
	public int StartHour { get => _startHour.Value; set => _startHour.Value = value; }
	public int EndHour { get => _endHour.Value; set => _endHour.Value = value; }
	public BlackSheepCloseModes ClosingMode { get => _blackSheepMode.Value; set => _blackSheepMode.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public NCandlesV6Strategy()
	{
		_candlesCount = Param(nameof(CandlesCount), 4)
		.SetGreaterThanZero()
		.SetDisplay("Candles", "Number of identical candles", "Pattern");

		_orderVolume = Param(nameof(OrderVolume), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Base order size", "Orders");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
		.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 4m)
		.SetDisplay("Trailing Step (pips)", "Minimum move before trailing updates", "Risk");

		_maxPositionVolume = Param(nameof(MaxPositionVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Max Position Volume", "Maximum absolute net position", "Risk");

		_useTradingHours = Param(nameof(UseTradingHours), false)
		.SetDisplay("Use Trading Hours", "Enable trading window", "Timing");

		_startHour = Param(nameof(StartHour), 11)
		.SetDisplay("Start Hour", "Hour when trading can start", "Timing");

		_endHour = Param(nameof(EndHour), 18)
		.SetDisplay("End Hour", "Hour when trading stops", "Timing");

		_blackSheepMode = Param(nameof(ClosingMode), BlackSheepCloseModes.All)
		.SetDisplay("Closing Mode", "Reaction to a black sheep candle", "Pattern");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe", "Pattern");
	}

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

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

		if (UseTradingHours && StartHour >= EndHour)
		throw new InvalidOperationException("Start hour must be less than end hour when trading window is enabled.");

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
		throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");

		Volume = OrderVolume;

		_pipSize = CalculatePipSize();

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

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

		_pipSize = 0m;
		ResetCounters();
		ResetPositionState();
		_blackSheepTriggered = false;
	}

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

		UpdateTrailingLevels(candle);

		if (ApplyRiskManagement(candle))
		return;

		var direction = GetDirection(candle);

		if (direction == 0)
		{
			if (_streakDirection != 0 && !_blackSheepTriggered)
			HandleBlackSheep(_streakDirection);

			ResetCounters();
			return;
		}

		if (_streakDirection == direction)
		{
			if (direction == 1)
			{
				_bullCount = Math.Min(CandlesCount, _bullCount + 1);
				_bearCount = 0;
			}
			else
			{
				_bearCount = Math.Min(CandlesCount, _bearCount + 1);
				_bullCount = 0;
			}
		}
		else
		{
			if (_streakDirection != 0 && !_blackSheepTriggered)
			HandleBlackSheep(_streakDirection);

			_streakDirection = direction;
			_bullCount = direction == 1 ? 1 : 0;
			_bearCount = direction == -1 ? 1 : 0;
		}

		var allowTrading = !UseTradingHours || IsWithinTradingHours(candle.OpenTime);

		if (_bullCount >= CandlesCount && allowTrading)
		{
			EnterLong(candle.ClosePrice);
		}
		else if (_bearCount >= CandlesCount && allowTrading)
		{
			EnterShort(candle.ClosePrice);
		}
	}

	private void EnterLong(decimal price)
	{
		if (OrderVolume <= 0m)
		return;

		var volume = OrderVolume;

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

		var projected = Position + volume;

		if (projected > MaxPositionVolume)
		return;

		BuyMarket(volume);

		_entryPrice = price;
		_stopLossPrice = StopLossPips > 0m ? price - GetPriceOffset(StopLossPips) : null;
		_takeProfitPrice = TakeProfitPips > 0m ? price + GetPriceOffset(TakeProfitPips) : null;
		_trailingLong = null;
		_trailingShort = null;
		_blackSheepTriggered = false;
	}

	private void EnterShort(decimal price)
	{
		if (OrderVolume <= 0m)
		return;

		var volume = OrderVolume;

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

		var projected = Position - volume;

		if (Math.Abs(projected) > MaxPositionVolume)
		return;

		SellMarket(volume);

		_entryPrice = price;
		_stopLossPrice = StopLossPips > 0m ? price + GetPriceOffset(StopLossPips) : null;
		_takeProfitPrice = TakeProfitPips > 0m ? price - GetPriceOffset(TakeProfitPips) : null;
		_trailingLong = null;
		_trailingShort = null;
		_blackSheepTriggered = false;
	}

	private void HandleBlackSheep(int direction)
	{
		if (direction == 0 || _blackSheepTriggered)
		return;

		switch (ClosingMode)
		{
			case BlackSheepCloseModes.All:
			{
				ClosePosition();
				break;
			}

			case BlackSheepCloseModes.Opposite:
			{
				if (direction == 1 && Position < 0m)
				{
					BuyMarket(Math.Abs(Position));
					ResetPositionState();
				}
				else if (direction == -1 && Position > 0m)
				{
					SellMarket(Math.Abs(Position));
					ResetPositionState();
				}

				break;
			}

			case BlackSheepCloseModes.Unidirectional:
			{
				if (direction == 1 && Position > 0m)
				{
					SellMarket(Math.Abs(Position));
					ResetPositionState();
				}
				else if (direction == -1 && Position < 0m)
				{
					BuyMarket(Math.Abs(Position));
					ResetPositionState();
				}

				break;
			}
		}

		_blackSheepTriggered = true;
	}

	private void ClosePosition()
	{
		if (Position > 0m)
		{
			SellMarket(Math.Abs(Position));
			ResetPositionState();
		}
		else if (Position < 0m)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
		}
	}

	private void UpdateTrailingLevels(ICandleMessage candle)
	{
		var trailingStop = GetPriceOffset(TrailingStopPips);

		if (trailingStop <= 0m)
		return;

		var trailingStep = GetPriceOffset(TrailingStepPips);

		if (Position > 0m)
		{
			var profit = candle.ClosePrice - _entryPrice;

			if (profit > trailingStop + trailingStep)
			{
				var candidate = candle.ClosePrice - trailingStop;

				if (_trailingLong == null || candidate > _trailingLong.Value + trailingStep)
				_trailingLong = candidate;
			}
		}
		else if (Position < 0m)
		{
			var profit = _entryPrice - candle.ClosePrice;

			if (profit > trailingStop + trailingStep)
			{
				var candidate = candle.ClosePrice + trailingStop;

				if (_trailingShort == null || candidate < _trailingShort.Value - trailingStep)
				_trailingShort = candidate;
			}
		}
	}

	private bool ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopLossPrice is decimal longSl && candle.LowPrice <= longSl)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal longTp && candle.HighPrice >= longTp)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_trailingLong is decimal trail && candle.LowPrice <= trail)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			var absPosition = Math.Abs(Position);

			if (_stopLossPrice is decimal shortSl && candle.HighPrice >= shortSl)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal shortTp && candle.LowPrice <= shortTp)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}

			if (_trailingShort is decimal trail && candle.HighPrice >= trail)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}
		}

		return false;
	}

	private void ResetCounters()
	{
		_streakDirection = 0;
		_bullCount = 0;
		_bearCount = 0;
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_trailingLong = null;
		_trailingShort = null;
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.TimeOfDay.Hours;
		return hour >= StartHour && hour <= EndHour;
	}

	private decimal GetPriceOffset(decimal pips)
	{
		if (pips <= 0m)
		return 0m;

		return pips * _pipSize;
	}

	private static int GetDirection(ICandleMessage candle)
	{
		if (candle.ClosePrice > candle.OpenPrice)
		return 1;

		if (candle.ClosePrice < candle.OpenPrice)
		return -1;

		return 0;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
		return 1m;

		var decimals = CountDecimals(step);

		return decimals == 3 || decimals == 5
		? step * 10m
		: step;
	}

	private static int CountDecimals(decimal value)
	{
		value = Math.Abs(value);
		var count = 0;

		while (value != Math.Truncate(value) && count < 10)
		{
			value *= 10m;
			count++;
		}

		return count;
	}
}