Ver en GitHub

Estrategia de Secuencia de N Velas

Descripción general

La estrategia de Secuencia de N Velas replica el comportamiento del experto MetaTrader original "N-_Candles_v7" usando la API de alto nivel de StockSharp. Monitorea velas finalizadas y busca un número configurable de cuerpos consecutivos alcistas o bajistas. Cuando hay una racha calificada presente, la estrategia abre una posición en la misma dirección y la gestiona con take profit, stop loss, trailing stop, filtro de horas de trading y bloqueo de beneficio flotante configurables.

Lógica de trading

  • Evalúa cada vela finalizada y la clasifica como alcista, bajista o neutral (doji). Las velas neutras reinician el contador de racha y pueden activar el comportamiento de "oveja negra".
  • Mantiene un recuento continuo de velas consecutivas con la misma dirección de cuerpo. Una vez que el recuento alcanza el umbral configurado, la dirección actual se convierte en el patrón activo.
  • Cuando hay una racha alcista activa, la estrategia intenta abrir una posición larga; cuando hay una racha bajista activa, intenta abrir una posición corta. Solo se mantiene una posición neta a la vez.
  • Si una vela rompe la dirección uniforme ("oveja negra"), la estrategia reacciona según el modo de cierre seleccionado: cerrar todo, cerrar solo posiciones opuestas, o cerrar solo posiciones alineadas con la racha anterior.
  • Opcionalmente, restringe las entradas a una ventana de trading definida por horas de inicio y fin (inclusivas).
  • Monitorea continuamente la posición abierta para take profit, stop loss, actualizaciones de trailing stop y el umbral de beneficio flotante.

Gestión de posición y riesgo

  • El stop inicial de protección y el objetivo se calculan a partir de distancias en pips convertidas con el paso de precio del instrumento. Estos niveles se recalculan cada vez que se abre una nueva posición.
  • La lógica del trailing stop imita el experto original: una vez que el precio recorre la distancia de trailing más el paso, el stop se mueve para mantener la distancia de trailing.
  • Un guardián de beneficio flotante (MinProfit) cierra toda la posición una vez que el beneficio abierto supera el valor configurado.
  • El parámetro MaxPositionVolume evita entradas si el volumen solicitado supera el límite permitido. MaxPositions funciona como protección contra la re-entrada cuando ya hay una posición activa.
  • Todas las salidas llaman a órdenes de mercado para aplanar la posición neta porque la estrategia de StockSharp opera en un entorno de compensación.

Parámetros

Nombre Descripción
ConsecutiveCandles Número de velas con dirección idéntica necesarias para activar una señal.
OrderVolume Volumen de orden de mercado usado para entradas y salidas.
TakeProfitPips Distancia de take profit expresada en pips. Poner en cero para deshabilitar.
StopLossPips Distancia de stop loss expresada en pips. Poner en cero para deshabilitar.
TrailingStopPips Distancia del trailing stop. Poner en cero para deshabilitar el trailing.
TrailingStepPips Distancia adicional requerida antes de que se mueva el trailing stop.
MaxPositions Número máximo de entradas simultáneas por patrón (la estrategia mantiene una sola posición neta).
MaxPositionVolume Límite superior para el volumen neto permitido.
UseTradeHours / StartHour / EndHour Habilitar y configurar la ventana de tiempo de trading (inclusiva).
MinProfit Umbral de beneficio flotante que activa una salida completa.
ClosingBehavior Define cómo reaccionar cuando aparece una vela de "oveja negra".
CandleType La serie de velas usada para los cálculos.

Notas y supuestos

  • La estrategia opera con posiciones netas; no se crean múltiples tickets de estilo cobertura. Esto difiere del experto original donde varias posiciones cubiertas podían coexistir.
  • El beneficio flotante se aproxima como (precio actual - precio de entrada) * volumen para posiciones largas y lo inverso para posiciones cortas.
  • La conversión de pips depende del PriceStep del instrumento. Para símbolos donde no se proporciona el paso mínimo, se asume un pip predeterminado de 0.0001.
  • No se proporciona portación a Python, según lo solicitado.
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>
/// Strategy that searches for N identical candles in a row and trades in the direction of the streak.
/// Implements optional take profit, stop loss, trailing stop, trading hours filter and profit lock.
/// </summary>
public class NCandlesSequenceStreakStrategy : Strategy
{
	private readonly StrategyParam<int> _consecutiveCandles;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _maxPositionVolume;
	private readonly StrategyParam<bool> _useTradeHours;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<ClosingModes> _closingBehavior;
	private readonly StrategyParam<DataType> _candleType;

	private int _streakCount;
	private int _lastDirection;
	private int _patternDirection;
	private int _entriesInDirection;
	private bool _blackSheepTriggered;
	private bool _hasPosition;
	private decimal _entryPrice;
	private decimal _pipSize;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Defines how positions are closed when a "black sheep" candle appears.
	/// </summary>
	public enum ClosingModes
	{
		/// <summary>Close every open position.</summary>
		All,

		/// <summary>Close positions opposite to the previously detected streak.</summary>
		Opposite,

		/// <summary>Close positions that follow the previously detected streak.</summary>
		SameDirection
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="NCandlesSequenceStreakStrategy"/> class.
	/// </summary>
	public NCandlesSequenceStreakStrategy()
	{
		_consecutiveCandles = Param(nameof(ConsecutiveCandles), 4)
		.SetGreaterThanZero()
		.SetDisplay("Consecutive Candles", "Number of candles with identical direction", "Pattern")
		
		.SetOptimize(2, 10, 1);

		_orderVolume = Param(nameof(OrderVolume), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume used for market orders", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit (pips)", "Distance for the take profit target", "Risk")
		
		.SetOptimize(10, 200, 10);

		_stopLossPips = Param(nameof(StopLossPips), 50)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss (pips)", "Distance for the protective stop", "Risk")
		
		.SetOptimize(10, 200, 10);

		_trailingStopPips = Param(nameof(TrailingStopPips), 10)
		.SetDisplay("Trailing Stop (pips)", "Distance used when trailing is active", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 4)
		.SetDisplay("Trailing Step (pips)", "Additional distance before moving the trailing stop", "Risk");

		_maxPositions = Param(nameof(MaxPositions), 2)
		.SetGreaterThanZero()
		.SetDisplay("Max Positions", "Maximum number of sequential entries in the same direction", "Risk");

		_maxPositionVolume = Param(nameof(MaxPositionVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Max Position Volume", "Maximum volume allowed for an open position", "Risk");

		_useTradeHours = Param(nameof(UseTradeHours), false)
		.SetDisplay("Use Trade Hours", "Enable intraday trading window", "Timing");

		_startHour = Param(nameof(StartHour), 11)
		.SetDisplay("Start Hour", "First trading hour (0-23)", "Timing");

		_endHour = Param(nameof(EndHour), 18)
		.SetDisplay("End Hour", "Last trading hour (0-23)", "Timing");

		_minProfit = Param(nameof(MinProfit), 3m)
		.SetDisplay("Min Profit", "Close positions when floating profit exceeds this value", "Risk")
		
		.SetOptimize(1m, 20m, 1m);

		_closingBehavior = Param(nameof(ClosingBehavior), ClosingModes.All)
		.SetDisplay("Black Sheep Closing", "Reaction when the streak is broken", "Pattern");

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

	/// <summary>
	/// Required number of candles with the same direction.
	/// </summary>
	public int ConsecutiveCandles
	{
		get => _consecutiveCandles.Value;
		set => _consecutiveCandles.Value = value;
	}

	/// <summary>
	/// Volume for market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

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

	/// <summary>
	/// Additional step before the trailing stop is moved.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Maximum number of consecutive entries in the same direction.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Maximum allowed volume for an open position.
	/// </summary>
	public decimal MaxPositionVolume
	{
		get => _maxPositionVolume.Value;
		set => _maxPositionVolume.Value = value;
	}

	/// <summary>
	/// Enables the trading hours filter.
	/// </summary>
	public bool UseTradeHours
	{
		get => _useTradeHours.Value;
		set => _useTradeHours.Value = value;
	}

	/// <summary>
	/// First trading hour (inclusive).
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Last trading hour (inclusive).
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Minimum floating profit that forces the strategy to close all positions.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// How to handle positions when the streak is broken.
	/// </summary>
	public ClosingModes ClosingBehavior
	{
		get => _closingBehavior.Value;
		set => _closingBehavior.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </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();
		ResetState();
	}

	private void ResetState()
	{
		_streakCount = 0;
		_lastDirection = 0;
		_patternDirection = 0;
		_entriesInDirection = 0;
		_blackSheepTriggered = false;
		_hasPosition = false;
		_entryPrice = 0m;
		_pipSize = 0m;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
	}

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

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

		_pipSize = CalculatePipSize();

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

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

		// no indicators to check formation

		UpdateTrailingStops(candle);
		ManageFloatingProfit(candle);

		var direction = GetCandleDirection(candle);

		if (direction == 0)
		{
			HandlePatternBreak();
			_lastDirection = 0;
			_streakCount = 0;
			return;
		}

		if (_lastDirection == direction)
		{
			_streakCount++;
		}
		else
		{
			if (_lastDirection != 0)
			HandlePatternBreak();

			_lastDirection = direction;
			_streakCount = 1;
		}

		if (_streakCount >= ConsecutiveCandles)
		{
			if (_patternDirection != direction)
			{
				_patternDirection = direction;
				_entriesInDirection = 0;
				_blackSheepTriggered = false;
			}

			if (direction > 0)
			TryEnterLong(candle);
			else
			TryEnterShort(candle);
		}

		ManageExits(candle);
	}

	private void ManageFloatingProfit(ICandleMessage candle)
	{
		if (MinProfit <= 0m || Position == 0m || !_hasPosition)
		return;

		var floating = CalculateOpenProfit(candle.ClosePrice);
		if (floating >= MinProfit)
		ClosePosition();
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		if (OrderVolume <= 0m || OrderVolume > MaxPositionVolume)
		return;

		if (_entriesInDirection >= MaxPositions)
		return;

		if (UseTradeHours && !IsWithinTradeHours(candle.CloseTime))
		return;

		if (Position < 0m)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
			return;
		}

		if (Position != 0m)
		return;

		BuyMarket(OrderVolume);
		InitializeLongState(candle.ClosePrice);
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		if (OrderVolume <= 0m || OrderVolume > MaxPositionVolume)
		return;

		if (_entriesInDirection >= MaxPositions)
		return;

		if (UseTradeHours && !IsWithinTradeHours(candle.CloseTime))
		return;

		if (Position > 0m)
		{
			SellMarket(Position);
			ResetPositionState();
			return;
		}

		if (Position != 0m)
		return;

		SellMarket(OrderVolume);
		InitializeShortState(candle.ClosePrice);
	}

	private void InitializeLongState(decimal entryPrice)
	{
		_hasPosition = true;
		_entryPrice = entryPrice;
		_entriesInDirection = 1;
		_blackSheepTriggered = false;

		var stopDistance = StopLossPips > 0 ? ToPrice(StopLossPips) : (decimal?)null;
		var takeDistance = TakeProfitPips > 0 ? ToPrice(TakeProfitPips) : (decimal?)null;

		_longStop = stopDistance.HasValue ? entryPrice - stopDistance : null;
		_longTake = takeDistance.HasValue ? entryPrice + takeDistance : null;
		_shortStop = null;
		_shortTake = null;
	}

	private void InitializeShortState(decimal entryPrice)
	{
		_hasPosition = true;
		_entryPrice = entryPrice;
		_entriesInDirection = 1;
		_blackSheepTriggered = false;

		var stopDistance = StopLossPips > 0 ? ToPrice(StopLossPips) : (decimal?)null;
		var takeDistance = TakeProfitPips > 0 ? ToPrice(TakeProfitPips) : (decimal?)null;

		_shortStop = stopDistance.HasValue ? entryPrice + stopDistance : null;
		_shortTake = takeDistance.HasValue ? entryPrice - takeDistance : null;
		_longStop = null;
		_longTake = null;
	}

	private void ManageExits(ICandleMessage candle)
	{
		if (!_hasPosition)
		return;

		if (Position > 0m)
		{
			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket(Position);
				ResetPositionState();
				return;
			}

			if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
			{
				SellMarket(Position);
				ResetPositionState();
				return;
			}
		}
		else if (Position < 0m)
		{
			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return;
			}

			if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return;
			}
		}
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (!_hasPosition || TrailingStopPips <= 0 || _pipSize <= 0m)
		return;

		var distance = ToPrice(TrailingStopPips);
		var step = TrailingStepPips > 0 ? ToPrice(TrailingStepPips) : 0m;

		if (Position > 0m)
		{
			var threshold = candle.ClosePrice - (distance + step);
			if (candle.ClosePrice - _entryPrice > distance + step && (!_longStop.HasValue || _longStop.Value < threshold))
			_longStop = candle.ClosePrice - distance;
		}
		else if (Position < 0m)
		{
			var threshold = candle.ClosePrice + (distance + step);
			if (_entryPrice - candle.ClosePrice > distance + step && (!_shortStop.HasValue || _shortStop.Value > threshold))
			_shortStop = candle.ClosePrice + distance;
		}
	}

	private void HandlePatternBreak()
	{
		if (_patternDirection == 0 || _blackSheepTriggered)
		return;

		switch (ClosingBehavior)
		{
			case ClosingModes.All:
			ClosePosition();
			break;

			case ClosingModes.Opposite:
			if (_patternDirection > 0 && Position < 0m)
			ClosePosition();
			else if (_patternDirection < 0 && Position > 0m)
			ClosePosition();
			break;

			case ClosingModes.SameDirection:
			if (_patternDirection > 0 && Position > 0m)
			ClosePosition();
			else if (_patternDirection < 0 && Position < 0m)
			ClosePosition();
			break;
		}

		_blackSheepTriggered = true;
		_entriesInDirection = 0;
		_patternDirection = 0;
	}

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

		ResetPositionState();
	}

	private void ResetPositionState()
	{
		_hasPosition = Position != 0m;
		_entriesInDirection = _hasPosition ? 1 : 0;

		if (!_hasPosition)
		{
			_entryPrice = 0m;
			_longStop = null;
			_longTake = null;
			_shortStop = null;
			_shortTake = null;
		}
	}

	private decimal CalculateOpenProfit(decimal currentPrice)
	{
		if (!_hasPosition || Position == 0m)
		return 0m;

		var volume = Math.Abs(Position);
		return Position > 0m ? (currentPrice - _entryPrice) * volume : (_entryPrice - currentPrice) * volume;
	}

	private static int GetCandleDirection(ICandleMessage candle)
	{
		if (candle.OpenPrice < candle.ClosePrice)
		return 1;

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

		return 0;
	}

	private bool IsWithinTradeHours(DateTimeOffset time)
	{
		var hour = time.Hour;
		return hour >= StartHour && hour <= EndHour;
	}

	private decimal ToPrice(int pips)
	{
		return pips * _pipSize;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		return 0.0001m;

		var decimals = CountDecimals(step);
		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}

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

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

		return decimals;
	}
}