在 GitHub 上查看

N Candles v5 策略

概述

N Candles v5 策略会寻找连续同向的蜡烛,并在满足条件时沿同一方向开仓。 原始的 MQL 版本由 Vladimir Karputov 编写,此处将其迁移到 StockSharp 的 高级 API。策略仅处理已完成的蜡烛,可用于任意时间框架,默认使用 1 小时 K 线。

交易逻辑

  1. 每根蜡烛收盘后,策略判断其为阳线(收盘价高于开盘价)、阴线 (收盘价低于开盘价)或横盘(收盘价等于开盘价)。
  2. 连续阳线会增加多头计数并重置空头计数,连续阴线则相反;横盘蜡烛 会将两个计数同时归零。
  3. 当阳线计数达到 CandlesCount 且当前净持仓为零或为空头时,策略按 市价买入。若存在空头仓位,会先行平仓,再按照 TradeVolume 建立多头。
  4. 当阴线计数达到 CandlesCount 且当前净持仓为零或为多头时,策略按 市价卖出。若存在多头仓位,会先平仓,再建立空头。
  5. 只有在启用 UseTradingHours 并且时间位于 StartHourEndHour 之间时才允许新开仓。止盈、止损和追踪止损在时段之外仍然生效,以保证 风险控制。
  6. MaxNetVolume 限制了绝对仓位规模,避免持仓超过指定上限,保持了 原版脚本的风险约束。

风险控制

  • 止盈 / 止损 – 以点数表示,根据标的的价格步长转换为绝对价格。 将数值设为 0 可关闭对应功能。
  • 追踪止损 – 当价格相对入场价移动 TrailingStopPips 点时激活,之后 每当价格再前进 TrailingStepPips 点就会收紧止损水平。
  • 交易时段过滤UseTradingHours 打开后,策略仅在指定时段内开仓, 但会在任意时间继续处理已有仓位的风险。
  • 最大净仓位 – 绝对持仓不会超过 MaxNetVolume

参数

参数 说明 默认值
TradeVolume 新开仓时使用的交易量。 1
CandlesCount 触发信号所需的连续同向蜡烛数量。 3
TakeProfitPips 止盈距离(点),0 表示关闭。 50
StopLossPips 止损距离(点),0 表示关闭。 50
TrailingStopPips 激活追踪止损的距离(点),0 表示关闭。 10
TrailingStepPips 收紧追踪止损所需的追加移动距离(点)。 4
UseTradingHours 是否启用交易时段过滤。 true
StartHour 允许开仓的起始小时(0–23)。 11
EndHour 允许开仓的结束小时(0–23)。 18
MaxNetVolume 允许的最大净仓位规模。 2
CandleType 用于分析的蜡烛类型,默认 1 小时。 TimeSpan.FromHours(1)

使用建议

  • 策略通过 SubscribeCandles 订阅蜡烛数据,适用于提供蜡烛序列的任何市场 品种。
  • 由于逻辑基于已收盘蜡烛,在噪声较低的日内或更高级别时间框架上表现更 稳定。
  • 根据标的的最小价格变动和点差调整止盈、止损与追踪参数,避免过度紧密。
  • 在点差波动较大的品种上运行时,建议先用历史数据验证追踪止损设置,防止 因正常波动而提前出场。
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 a sequence of identical candles and opens trades in the same direction.
/// Implements optional take profit, stop loss, trailing stop and trading hour filter.
/// </summary>
public class NCandlesV5Strategy : Strategy
{
	private readonly StrategyParam<int> _candlesCount;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<bool> _useTradingHours;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _volumeParam;
	private readonly StrategyParam<decimal> _maxNetVolume;
	private readonly StrategyParam<DataType> _candleType;

	private int _bullishCount;
	private int _bearishCount;

	private decimal? _longEntryPrice;
	private decimal? _longTakeProfit;
	private decimal? _longStopLoss;
	private decimal? _longTrailingStop;

	private decimal? _shortEntryPrice;
	private decimal? _shortTakeProfit;
	private decimal? _shortStopLoss;
	private decimal? _shortTrailingStop;

	/// <summary>
	/// Initializes a new instance of <see cref="NCandlesV5Strategy"/>.
	/// </summary>
	public NCandlesV5Strategy()
	{
		_volumeParam = Param(nameof(TradeVolume), 1m)
		.SetDisplay("Trade Volume", "Order volume for entries", "Trading")
		.SetGreaterThanZero();

		_candlesCount = Param(nameof(CandlesCount), 3)
		.SetDisplay("Candles Count", "Number of identical candles required", "General")
		.SetGreaterThanZero();

		_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 activation distance", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 4m)
		.SetDisplay("Trailing Step (pips)", "Increment required to tighten trailing stop", "Risk");

		_useTradingHours = Param(nameof(UseTradingHours), true)
		.SetDisplay("Use Trading Hours", "Enable trading session filter", "Trading");

		_startHour = Param(nameof(StartHour), 11)
		.SetRange(0, 23)
		.SetDisplay("Start Hour", "Hour when trading is allowed to start", "Trading");

		_endHour = Param(nameof(EndHour), 18)
		.SetRange(0, 23)
		.SetDisplay("End Hour", "Hour when trading is allowed to stop", "Trading");

		_maxNetVolume = Param(nameof(MaxNetVolume), 2m)
		.SetDisplay("Max Net Volume", "Maximum absolute net position", "Risk")
		.SetGreaterThanZero();

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

		Volume = _volumeParam.Value;
	}

	/// <summary>
	/// Trade volume used for new entries.
	/// </summary>
	public decimal TradeVolume
	{
		get => _volumeParam.Value;
		set
		{
			_volumeParam.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Number of consecutive identical candles required for a signal.
	/// </summary>
	public int CandlesCount
	{
		get => _candlesCount.Value;
		set => _candlesCount.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

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

	/// <summary>
	/// Trailing step distance in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Enables the trading hour filter.
	/// </summary>
	public bool UseTradingHours
	{
		get => _useTradingHours.Value;
		set => _useTradingHours.Value = value;
	}

	/// <summary>
	/// First hour of the allowed trading window.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Last hour of the allowed trading window.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Maximum absolute net position allowed.
	/// </summary>
	public decimal MaxNetVolume
	{
		get => _maxNetVolume.Value;
		set => _maxNetVolume.Value = value;
	}

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

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

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

		Volume = TradeVolume;
		ResetState();
	}

	/// <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 hours filter is enabled.");

		Volume = TradeVolume;

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Work only with completed candles to avoid premature signals.
		if (candle.State != CandleStates.Finished)
		return;

		// Refresh trailing and exit logic before looking for new opportunities.
		UpdateRiskManagement(candle);

		var direction = GetDirection(candle);
		// Track bullish and bearish streak length.


		if (direction == 1)
		{
			_bullishCount++;
			_bearishCount = 0;
		}
		else if (direction == -1)
		{
			_bearishCount++;
			_bullishCount = 0;
		}
		else
		{
			_bullishCount = 0;
			_bearishCount = 0;
		}

		var tradingAllowed = !UseTradingHours || (candle.OpenTime.Hour >= StartHour && candle.OpenTime.Hour <= EndHour);
		// Skip entries outside the configured session window.

		if (!tradingAllowed)
		return;

		var volume = TradeVolume;
		if (volume <= 0m)
		return;

		var step = Security?.PriceStep ?? 1m;
		// Use instrument price step to translate pip distances to absolute prices.

		if (_bullishCount >= CandlesCount && Position <= 0m)
		{
			// Enter long after detecting the required number of bullish candles in a row.
			var orderVolume = volume + Math.Max(0m, -Position);
			if (orderVolume > 0m && Math.Abs(Position + orderVolume) <= MaxNetVolume)
			{
				BuyMarket();
				SetupLongState(candle, step);
			}

			ResetCounters();
		}
		else if (_bearishCount >= CandlesCount && Position >= 0m)
		{
			// Enter short after detecting the required number of bearish candles in a row.
			var orderVolume = volume + Math.Max(0m, Position);
			if (orderVolume > 0m && Math.Abs(Position - orderVolume) <= MaxNetVolume)
			{
				SellMarket();
				SetupShortState(candle, step);
			}

			ResetCounters();
		}
	}

	private void UpdateRiskManagement(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			ManageLongPosition(candle);
		}
		else
		{
			ClearLongState();
		}

		if (Position < 0m)
		{
			ManageShortPosition(candle);
		}
		else
		{
			ClearShortState();
		}
	}

	private void ManageLongPosition(ICandleMessage candle)
	{
		if (_longEntryPrice is null)
			// Capture entry price if it was not stored yet (for example after restart).
			_longEntryPrice = candle.ClosePrice;

		var step = Security?.PriceStep ?? 1m;
		var close = candle.ClosePrice;
		var high = candle.HighPrice;
		var low = candle.LowPrice;

		var trailingDistance = TrailingStopPips > 0m ? TrailingStopPips * step : 0m;
		var trailingStep = TrailingStepPips > 0m ? TrailingStepPips * step : 0m;

		if (TrailingStopPips > 0m && _longEntryPrice is decimal entry)
		{
			// Update trailing stop level according to the latest candle.

			if (_longTrailingStop is null)
			{
				if (close - trailingDistance > entry)
				_longTrailingStop = entry;
			}
			else
			{
				var newLevel = close - trailingDistance;
				if (newLevel - trailingStep > _longTrailingStop)
				_longTrailingStop = newLevel;
			}
		}
		else
		{
			_longTrailingStop = null;
		}

		var exitVolume = Position > 0m ? Position : 0m;
		var closed = false;

		// Exit the long position when any protective target is triggered.
		if (!closed && _longTakeProfit is decimal takeProfit && high >= takeProfit)
		{
			if (exitVolume > 0m)
			SellMarket();
			closed = true;
		}

		if (!closed && _longStopLoss is decimal stopLoss && low <= stopLoss)
		{
			if (exitVolume > 0m)
			SellMarket();
			closed = true;
		}

		if (!closed && _longTrailingStop is decimal trailingStop && low <= trailingStop)
		{
			if (exitVolume > 0m)
			SellMarket();
			closed = true;
		}

		if (closed)
			ClearLongState();
	}

	private void ManageShortPosition(ICandleMessage candle)
	{
		if (_shortEntryPrice is null)
			// Capture entry price if it was not stored yet (for example after restart).
			_shortEntryPrice = candle.ClosePrice;

		var step = Security?.PriceStep ?? 1m;
		var close = candle.ClosePrice;
		var high = candle.HighPrice;
		var low = candle.LowPrice;

		var trailingDistance = TrailingStopPips > 0m ? TrailingStopPips * step : 0m;
		var trailingStep = TrailingStepPips > 0m ? TrailingStepPips * step : 0m;

		if (TrailingStopPips > 0m && _shortEntryPrice is decimal entry)
		{
			// Update trailing stop level for the active short position.

			if (_shortTrailingStop is null)
			{
				if (close + trailingDistance < entry)
				_shortTrailingStop = entry;
			}
			else
			{
				var newLevel = close + trailingDistance;
				if (newLevel + trailingStep < _shortTrailingStop)
				_shortTrailingStop = newLevel;
			}
		}
		else
		{
			_shortTrailingStop = null;
		}

		var exitVolume = Position < 0m ? -Position : 0m;
		var closed = false;

		// Exit the short position when any protective target is triggered.
		if (!closed && _shortTakeProfit is decimal takeProfit && low <= takeProfit)
		{
			if (exitVolume > 0m)
			BuyMarket();
			closed = true;
		}

		if (!closed && _shortStopLoss is decimal stopLoss && high >= stopLoss)
		{
			if (exitVolume > 0m)
			BuyMarket();
			closed = true;
		}

		if (!closed && _shortTrailingStop is decimal trailingStop && high >= trailingStop)
		{
			if (exitVolume > 0m)
			BuyMarket();
			closed = true;
		}

		if (closed)
			ClearShortState();
	}

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

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

		return 0;
	}

	private void SetupLongState(ICandleMessage candle, decimal step)
	{
		var entryPrice = candle.ClosePrice;
		// Store reference levels for long-side risk management.
		_longEntryPrice = entryPrice;
		_longTakeProfit = TakeProfitPips > 0m ? entryPrice + TakeProfitPips * step : null;
		_longStopLoss = StopLossPips > 0m ? entryPrice - StopLossPips * step : null;
		_longTrailingStop = null;

		ClearShortState();
	}

	private void SetupShortState(ICandleMessage candle, decimal step)
	{
		var entryPrice = candle.ClosePrice;
		// Store reference levels for short-side risk management.
		_shortEntryPrice = entryPrice;
		_shortTakeProfit = TakeProfitPips > 0m ? entryPrice - TakeProfitPips * step : null;
		_shortStopLoss = StopLossPips > 0m ? entryPrice + StopLossPips * step : null;
		_shortTrailingStop = null;

		ClearLongState();
	}

	private void ClearLongState()
	{
		_longEntryPrice = null;
		_longTakeProfit = null;
		_longStopLoss = null;
		_longTrailingStop = null;
	}

	private void ClearShortState()
	{
		_shortEntryPrice = null;
		_shortTakeProfit = null;
		_shortStopLoss = null;
		_shortTrailingStop = null;
	}

	private void ResetState()
	{
		ResetCounters();
		ClearLongState();
		ClearShortState();
	}

	private void ResetCounters()
	{
		_bullishCount = 0;
		_bearishCount = 0;
	}
}