在 GitHub 上查看

N根K线 v2

概述

该策略会寻找一段固定数量的连续同向K线。当连续收盘方向达到设定阈值后,系统会按照趋势方向开仓。实现完全遵循 MetaTrader 5 的 "N- candles v2" 智能交易顾问,并且只处理已经收盘的K线以减少噪音信号。

策略逻辑

  1. 订阅选定周期的K线,并等待其完全收盘。
  2. 将每根K线分类为阳线、阴线或十字线(无方向)。十字线会把连线计数器清零。
  3. 维护一个连续同方向K线的计数器。
  4. 当计数器达到 CandlesCount 时,以同样方向提交市价单。下单数量会把目标 LotSize 与当前反向仓位合并,使净头寸符合理想方向与数量。
  5. 记录入场价,并根据设定的止损与止盈距离初始化保护水平。
  6. 每根新K线都会更新可选的移动止损,并在价格触及止损、移动止损或止盈时离场。

仓位管理

  • 初始止损和止盈基于价格最小变动单位 (Security.PriceStep)。若参数为0则表示禁用该保护。
  • 移动止损为可选项。启用后,当价格至少再向有利方向移动 TrailingStepPips 时,会按 TrailingStopPips 的距离上调或下调止损。
  • 仓位平仓后会清空所有缓存的价格水平,必须重新等待新的连续K线序列才会再次进场。

参数

名称 说明 默认值
CandlesCount 需要连续同方向收盘的K线数量。 3
LotSize 每次进场的头寸规模,系统会自动对冲反向持仓。 1
TakeProfitPips 自入场价起的止盈距离(价格步长)。 50
StopLossPips 自入场价起的止损距离(价格步长)。 50
TrailingStopPips 移动止损距离(价格步长),0 表示禁用。 10
TrailingStepPips 触发移动止损调整所需的额外价格移动。 4
CandleType 用于计算信号的K线周期。 1小时K线

备注

  • 策略依赖于合约的 PriceStep 信息。如果该值为0,则使用1作为后备,与原始脚本保持一致。
  • 只有在K线收盘后才会生成信号,从而保证回测与实盘之间的可比性。
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>
/// Trades when a configurable number of consecutive candles share the same direction.
/// Applies fixed stop-loss, take-profit and optional trailing stop in price steps.
/// </summary>
public class NCandlesV2Strategy : Strategy
{
	private readonly StrategyParam<int> _candlesCount;
	private readonly StrategyParam<decimal> _lotSize;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private int _streakLength;
	private int _streakDirection;
	private int _currentPositionDirection;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	public int CandlesCount
	{
		get => _candlesCount.Value;
		set => _candlesCount.Value = value;
	}

	public decimal LotSize
	{
		get => _lotSize.Value;
		set => _lotSize.Value = value;
	}

	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

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

	public NCandlesV2Strategy()
	{
		_candlesCount = Param(nameof(CandlesCount), 3)
			.SetGreaterThanZero()
			.SetDisplay("Candles in Row", "Number of identical candles required", "Entry");

		_lotSize = Param(nameof(LotSize), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Lot Size", "Position size used for entries", "Risk");

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

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

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

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

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for analysis", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		ResetState();
	}

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

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

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

		// Wait until the strategy is fully initialized and allowed to trade.
		// Update trailing logic and close the position if protective levels are hit.
		if (ManageOpenPosition(candle))
			return;

		var direction = GetCandleDirection(candle);

		// Doji candles reset the streak because they do not show clear direction.
		if (direction == 0)
		{
			ResetStreak();
			return;
		}

		// Maintain the running count of identical candles.
		if (direction == _streakDirection)
			_streakLength++;
		else
		{
			_streakDirection = direction;
			_streakLength = 1;
		}

		// Enter only after the required number of matching candles is observed.
		if (_streakLength < CandlesCount)
			return;

		if (direction > 0)
			TryOpenLong(candle);
		else
			TryOpenShort(candle);
	}

	private bool ManageOpenPosition(ICandleMessage candle)
	{
		// Reset cached values once the position is flat.
		if (Position == 0)
		{
			_currentPositionDirection = 0;
			_stopPrice = null;
			_takePrice = null;
			_entryPrice = 0m;
			return false;
		}

		var pip = GetPipSize();
		var trailingStep = TrailingStepPips * pip;

		if (_currentPositionDirection > 0)
		{
			// Raise the stop for long trades when price advances far enough.
			if (TrailingStopPips > 0)
			{
				var desired = candle.ClosePrice - TrailingStopPips * pip;
				if (_stopPrice is decimal stop && desired - trailingStep > stop)
					_stopPrice = desired;
			}

			// Close long positions if take-profit or stop-loss levels are reached.
			if (_takePrice is decimal take && candle.HighPrice >= take)
				return ExitPosition();

			if (_stopPrice is decimal stopLoss && candle.LowPrice <= stopLoss)
				return ExitPosition();
		}
		else if (_currentPositionDirection < 0)
		{
			// Lower the stop for short trades when price keeps moving down.
			if (TrailingStopPips > 0)
			{
				var desired = candle.ClosePrice + TrailingStopPips * pip;
				if (_stopPrice is decimal stop && desired + trailingStep < stop)
					_stopPrice = desired;
			}

			// Close short positions if take-profit or stop-loss levels are reached.
			if (_takePrice is decimal take && candle.LowPrice <= take)
				return ExitPosition();

			if (_stopPrice is decimal stopLoss && candle.HighPrice >= stopLoss)
				return ExitPosition();
		}

		return false;
	}

	private void TryOpenLong(ICandleMessage candle)
	{
		if (Position > 0)
			return;

		if (Position < 0)
			BuyMarket();

		BuyMarket();
		SetPositionState(candle.ClosePrice, 1);
	}

	private void TryOpenShort(ICandleMessage candle)
	{
		if (Position < 0)
			return;

		if (Position > 0)
			SellMarket();

		SellMarket();
		SetPositionState(candle.ClosePrice, -1);
	}

	private bool ExitPosition()
	{
		// Close the active position and clear the cached trade state.
		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();

		ResetState();
		return true;
	}

	private void SetPositionState(decimal price, int direction)
	{
		// Remember the entry direction and compute initial protective levels.
		_currentPositionDirection = direction;
		_entryPrice = price;

		var pip = GetPipSize();

		if (direction > 0)
		{
			_stopPrice = StopLossPips > 0 ? price - StopLossPips * pip : (TrailingStopPips > 0 ? price : null);
			_takePrice = TakeProfitPips > 0 ? price + TakeProfitPips * pip : null;
		}
		else
		{
			_stopPrice = StopLossPips > 0 ? price + StopLossPips * pip : (TrailingStopPips > 0 ? price : null);
			_takePrice = TakeProfitPips > 0 ? price - TakeProfitPips * pip : null;
		}
	}

	private void ResetState()
	{
		ResetStreak();
		_currentPositionDirection = 0;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}

	private void ResetStreak()
	{
		_streakLength = 0;
		_streakDirection = 0;
	}

	private static int GetCandleDirection(ICandleMessage candle)
	{
		return candle.ClosePrice > candle.OpenPrice ? 1 : candle.ClosePrice < candle.OpenPrice ? -1 : 0;
	}

	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}