Открыть на GitHub

N свечей v2

Обзор

Стратегия отслеживает заданное количество подряд закрывшихся свечей одного направления. Когда длина серии достигает порога, алгоритм открывает рыночную позицию в сторону выявленного импульса. Реализация повторяет логику советника MetaTrader 5 "N- candles v2" и использует только закрытые свечи, чтобы избежать ложных сигналов.

Логика стратегии

  1. Подписаться на выбранный поток свечей и ожидать их полного закрытия.
  2. Классифицировать каждую свечу как бычью, медвежью или нейтральную (доджи). Доджи обнуляют счётчик серии.
  3. Вести скользящий счётчик одинаковых по направлению свечей.
  4. Когда счётчик достигает значения CandlesCount, отправить рыночный ордер в том же направлении. Объём заявки объединяет желаемый LotSize с противоположной позицией, чтобы итоговое нетто соответствовало требуемому знаку и величине.
  5. Сохранить цену входа и рассчитать защитные уровни по заданным расстояниям стоп-лосса и тейк-профита.
  6. На каждой новой свече обновлять трейлинг-стоп (если он включён) и закрывать позицию при достижении стоп-лосса, трейлинг-стопа или тейк-профита.

Управление позицией

  • Начальные стоп-лосс и тейк-профит измеряются в шагах цены (Security.PriceStep). Нулевое значение отключает соответствующий уровень.
  • Трейлинг-стоп необязателен. При активации стоп подтягивается на расстояние TrailingStopPips, когда цена проходит дополнительно как минимум TrailingStepPips по направлению позиции.
  • После закрытия позиции все кешированные уровни сбрасываются, и для нового входа требуется свежая серия свечей.

Параметры

Имя Описание Значение по умолчанию
CandlesCount Количество подряд идущих свечей одного направления перед входом. 3
LotSize Размер позиции для каждой сделки. Противоположный объём закрывается автоматически. 1
TakeProfitPips Расстояние тейк-профита в шагах цены от цены входа. 50
StopLossPips Расстояние стоп-лосса в шагах цены от цены входа. 50
TrailingStopPips Расстояние трейлинг-стопа в шагах цены. 0 — отключить. 10
TrailingStepPips Дополнительный путь цены перед подтягиванием трейлинг-стопа. 4
CandleType Таймфрейм свечей для расчётов сигналов. Свечи 1 часа

Примечания

  • Стратегия корректно работает с инструментами, у которых задан PriceStep. При значении 0 используется запасной вариант 1, повторяющий поведение исходного скрипта.
  • Сигналы формируются только на закрытых свечах, что обеспечивает сопоставимость результатов тестирования и реальной торговли.
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;
	}
}