Ver no GitHub

Estratégia N Candles v2

Visão geral

A estratégia busca um número configurável de candles consecutivos que fecham na mesma direção. Quando o comprimento da sequência é atingido, abre uma posição a mercado na direção do momentum detectado. A implementação espelha o expert advisor original do MetaTrader 5 "N- candles v2" e mantém a lógica focada em candles fechados para evitar sinais prematuros.

Lógica da estratégia

  1. Assinar a série de candles selecionada e aguardar barras completamente fechadas.
  2. Categorizar cada candle como altista, baixista ou neutro (doji). Candles doji reiniciam a sequência.
  3. Manter um contador acumulado de candles consecutivos com direção idêntica.
  4. Quando o contador atingir o limiar CandlesCount, enviar uma ordem a mercado na mesma direção. O tamanho da ordem combina o LotSize solicitado com qualquer exposição contrária, de modo que a posição líquida final tenha o sinal e a quantidade pretendidos.
  5. Armazenar o preço de entrada e inicializar os níveis de proteção usando as distâncias configuradas de stop-loss e take-profit.
  6. A cada novo candle, atualizar o trailing stop (se habilitado) e sair de posições quando o preço tocar o stop-loss, trailing stop ou take-profit.

Gestão de posição

  • O stop-loss e take-profit iniciais são medidos em passos de preço (Security.PriceStep). Uma distância zero desativa o nível correspondente.
  • O trailing stop é opcional. Quando habilitado, o stop é ajustado em TrailingStopPips assim que o preço se move favoravelmente pelo menos TrailingStepPips além da última localização do stop.
  • Fechar uma posição remove todos os níveis em cache, de modo que uma nova sequência é necessária para a próxima entrada.

Parâmetros

Nome Descrição Padrão
CandlesCount Número de candles consecutivos que devem fechar na mesma direção antes de negociar. 3
LotSize Tamanho da posição usado para cada entrada. A exposição contrária é fechada automaticamente. 1
TakeProfitPips Distância take-profit em passos de preço a partir do preço de entrada. 50
StopLossPips Distância stop-loss em passos de preço a partir do preço de entrada. 50
TrailingStopPips Distância do trailing stop em passos de preço. Defina como 0 para desabilitar o trailing. 10
TrailingStepPips Distância extra que o preço deve percorrer antes de ajustar o trailing stop. 4
CandleType Período de candles usado para cálculos de sinal. Candles de 1 hora

Notas

  • A estratégia funciona com qualquer instrumento que forneça um PriceStep válido. Se o instrumento reportar zero, 1 é usado como fallback, correspondendo ao comportamento do script original.
  • Os sinais são gerados apenas em candles concluídos, o que mantém o comportamento consistente entre backtesting e ambientes de trading ao vivo.
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;
	}
}