Ver no GitHub

Estratégia N Candles v6

Visão geral

A estratégia N Candles v6 monitora as velas concluídas mais recentes e procura sequências de direção idêntica. Quando o mercado imprime N velas de alta seguidas, a estratégia abre uma posição comprada, enquanto uma série de N velas de baixa produz uma entrada vendida. A lógica é inspirada no consultor especialista do MetaTrader N Candles v6.mq5 e é adaptada para a API de alto nível do StockSharp.

O algoritmo é projetado para qualquer símbolo que entregue velas padrão baseadas em tempo. Uma janela de trading configurável mantém a estratégia inativa fora da sessão desejada, mas a lógica ativa de trailing e saída continua a proteger uma posição aberta mesmo durante as horas bloqueadas.

Lógica de trading

  1. Subscrever ao tipo de vela configurado e processar apenas velas concluídas.
  2. Contar velas consecutivas de alta (Close > Open) e de baixa (Close < Open). Dojis reiniciam os contadores.
  3. Quando CandlesCount velas de alta aparecerem:
    • Verificar que a posição líquida projetada permaneça abaixo de MaxPositionVolume.
    • Enviar uma ordem de compra de mercado. Se uma posição vendida existir, o tamanho da ordem é aumentado para virar a posição para comprada em um único trade.
  4. Quando CandlesCount velas de baixa aparecerem:
    • Assegurar que a nova exposição vendida não excederá MaxPositionVolume.
    • Enviar uma ordem de venda de mercado e ampliar a ordem se uma posição comprada precisar ser fechada.
  5. Se a vela mais recente quebrar a sequência (a "ovelha negra"):
    • Aplicar o ClosingMode selecionado para fechar todas, as opostas ou as da mesma direção de uma vez.
  6. O trailing e as saídas protetoras são executados em cada vela:
    • Os níveis de stop-loss e take-profit são derivados de distâncias em pips e do passo de preço do instrumento.
    • O trailing stop se ativa após o preço se mover por TrailingStopPips + TrailingStepPips e apenas se engancha na direção favorável.
    • Qualquer violação do stop, take-profit, ou nível de trailing fecha imediatamente toda a posição.

Gerenciamento de risco

  • Stop Loss (pips) – converte a distância em pips em um offset de preço absoluto usando o passo de preço do símbolo (instrumentos de 5 e 3 dígitos são escalonados automaticamente).
  • Take Profit (pips) – fecha a posição após um movimento favorável do tamanho especificado.
  • Trailing Stop / Step (pips) – habilita proteção dinâmica assim que o trade atinge o limiar de lucro configurado. O passo deve ser diferente de zero quando o trailing está ativo.
  • Max Position Volume – limita a posição líquida absoluta. Sinais que violariam o limite são ignorados.
  • Closing Mode – determina como reagir quando uma vela não conforme aparece:
    • All – zerar toda a posição.
    • Opposite – fechar posições contra a direção da sequência (p.ex., fechar vendidas após uma sequência de alta se quebrar).
    • Unidirectional – fechar posições apenas na direção da sequência.
  • Janela de trading – a estratégia abre novos trades apenas quando a hora de abertura da vela está entre StartHour e EndHour (inclusive). As saídas protetoras continuam operando mesmo quando novos trades estão bloqueados.

Parâmetros

Nome Padrão Descrição
CandlesCount 3 Número de velas idênticas necessárias para um sinal.
OrderVolume 0.01 Tamanho base da ordem de mercado. A exposição oposta é fechada antes de estabelecer um novo trade.
TakeProfitPips 50 Distância do take-profit em pips. 0 desabilita o alvo.
StopLossPips 50 Distância do stop-loss em pips. 0 desabilita o stop.
TrailingStopPips 10 Distância do trailing stop em pips. 0 desabilita o trailing.
TrailingStepPips 4 Melhoria mínima de preço antes que o nível de trailing se mova. Deve ser > 0 quando o trailing está habilitado.
MaxPositionVolume 2 Máxima posição líquida absoluta.
UseTradingHours true Habilita filtragem de janela de trading.
StartHour 11 Início da sessão de trading (0-23).
EndHour 18 Fim da sessão de trading (0-23).
ClosingMode All Comportamento quando uma vela ovelha negra aparece.
CandleType Velas de 1 hora Tipo de dados usado para geração de sinais.

Notas

  • A conversão de pips é baseada no PriceStep do instrumento. Para cotações de 5 e 3 dígitos, a estratégia multiplica o passo por dez para corresponder à definição tradicional de pip.
  • Chamar StartProtection() durante o início para habilitar os serviços de salvaguarda do StockSharp (cancelar-ao-parar, segurança de reconexão, etc.).
  • A lógica usa a posição líquida (Strategy.Position) e portanto opera corretamente em contas de netting. O comportamento estilo hedging pode ser emulado definindo um MaxPositionVolume alto.
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 runs of identical candles and trades in the direction of the streak.
/// </summary>
public class NCandlesV6Strategy : Strategy
{
	/// <summary>
	/// Defines how positions are closed when a candle breaks the streak.
	/// </summary>
	public enum BlackSheepCloseModes
	{
		/// <summary>
		/// Close every open position regardless of direction.
		/// </summary>
		All,

		/// <summary>
		/// Close only positions that oppose the detected streak.
		/// </summary>
		Opposite,

		/// <summary>
		/// Close only positions that follow the detected streak.
		/// </summary>
		Unidirectional,
	}

	private readonly StrategyParam<int> _candlesCount;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _maxPositionVolume;
	private readonly StrategyParam<bool> _useTradingHours;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<BlackSheepCloseModes> _blackSheepMode;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal? _trailingLong;
	private decimal? _trailingShort;
	private int _streakDirection;
	private int _bullCount;
	private int _bearCount;
	private bool _blackSheepTriggered;

	public int CandlesCount { get => _candlesCount.Value; set => _candlesCount.Value = value; }
	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
	public decimal TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
	public decimal StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public decimal TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
	public decimal TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
	public decimal MaxPositionVolume { get => _maxPositionVolume.Value; set => _maxPositionVolume.Value = value; }
	public bool UseTradingHours { get => _useTradingHours.Value; set => _useTradingHours.Value = value; }
	public int StartHour { get => _startHour.Value; set => _startHour.Value = value; }
	public int EndHour { get => _endHour.Value; set => _endHour.Value = value; }
	public BlackSheepCloseModes ClosingMode { get => _blackSheepMode.Value; set => _blackSheepMode.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public NCandlesV6Strategy()
	{
		_candlesCount = Param(nameof(CandlesCount), 4)
		.SetGreaterThanZero()
		.SetDisplay("Candles", "Number of identical candles", "Pattern");

		_orderVolume = Param(nameof(OrderVolume), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Base order size", "Orders");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 4m)
		.SetDisplay("Trailing Step (pips)", "Minimum move before trailing updates", "Risk");

		_maxPositionVolume = Param(nameof(MaxPositionVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Max Position Volume", "Maximum absolute net position", "Risk");

		_useTradingHours = Param(nameof(UseTradingHours), false)
		.SetDisplay("Use Trading Hours", "Enable trading window", "Timing");

		_startHour = Param(nameof(StartHour), 11)
		.SetDisplay("Start Hour", "Hour when trading can start", "Timing");

		_endHour = Param(nameof(EndHour), 18)
		.SetDisplay("End Hour", "Hour when trading stops", "Timing");

		_blackSheepMode = Param(nameof(ClosingMode), BlackSheepCloseModes.All)
		.SetDisplay("Closing Mode", "Reaction to a black sheep candle", "Pattern");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe", "Pattern");
	}

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

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

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
		throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");

		Volume = OrderVolume;

		_pipSize = CalculatePipSize();

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

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

		_pipSize = 0m;
		ResetCounters();
		ResetPositionState();
		_blackSheepTriggered = false;
	}

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

		UpdateTrailingLevels(candle);

		if (ApplyRiskManagement(candle))
		return;

		var direction = GetDirection(candle);

		if (direction == 0)
		{
			if (_streakDirection != 0 && !_blackSheepTriggered)
			HandleBlackSheep(_streakDirection);

			ResetCounters();
			return;
		}

		if (_streakDirection == direction)
		{
			if (direction == 1)
			{
				_bullCount = Math.Min(CandlesCount, _bullCount + 1);
				_bearCount = 0;
			}
			else
			{
				_bearCount = Math.Min(CandlesCount, _bearCount + 1);
				_bullCount = 0;
			}
		}
		else
		{
			if (_streakDirection != 0 && !_blackSheepTriggered)
			HandleBlackSheep(_streakDirection);

			_streakDirection = direction;
			_bullCount = direction == 1 ? 1 : 0;
			_bearCount = direction == -1 ? 1 : 0;
		}

		var allowTrading = !UseTradingHours || IsWithinTradingHours(candle.OpenTime);

		if (_bullCount >= CandlesCount && allowTrading)
		{
			EnterLong(candle.ClosePrice);
		}
		else if (_bearCount >= CandlesCount && allowTrading)
		{
			EnterShort(candle.ClosePrice);
		}
	}

	private void EnterLong(decimal price)
	{
		if (OrderVolume <= 0m)
		return;

		var volume = OrderVolume;

		if (Position < 0m)
		volume += Math.Abs(Position);

		var projected = Position + volume;

		if (projected > MaxPositionVolume)
		return;

		BuyMarket(volume);

		_entryPrice = price;
		_stopLossPrice = StopLossPips > 0m ? price - GetPriceOffset(StopLossPips) : null;
		_takeProfitPrice = TakeProfitPips > 0m ? price + GetPriceOffset(TakeProfitPips) : null;
		_trailingLong = null;
		_trailingShort = null;
		_blackSheepTriggered = false;
	}

	private void EnterShort(decimal price)
	{
		if (OrderVolume <= 0m)
		return;

		var volume = OrderVolume;

		if (Position > 0m)
		volume += Math.Abs(Position);

		var projected = Position - volume;

		if (Math.Abs(projected) > MaxPositionVolume)
		return;

		SellMarket(volume);

		_entryPrice = price;
		_stopLossPrice = StopLossPips > 0m ? price + GetPriceOffset(StopLossPips) : null;
		_takeProfitPrice = TakeProfitPips > 0m ? price - GetPriceOffset(TakeProfitPips) : null;
		_trailingLong = null;
		_trailingShort = null;
		_blackSheepTriggered = false;
	}

	private void HandleBlackSheep(int direction)
	{
		if (direction == 0 || _blackSheepTriggered)
		return;

		switch (ClosingMode)
		{
			case BlackSheepCloseModes.All:
			{
				ClosePosition();
				break;
			}

			case BlackSheepCloseModes.Opposite:
			{
				if (direction == 1 && Position < 0m)
				{
					BuyMarket(Math.Abs(Position));
					ResetPositionState();
				}
				else if (direction == -1 && Position > 0m)
				{
					SellMarket(Math.Abs(Position));
					ResetPositionState();
				}

				break;
			}

			case BlackSheepCloseModes.Unidirectional:
			{
				if (direction == 1 && Position > 0m)
				{
					SellMarket(Math.Abs(Position));
					ResetPositionState();
				}
				else if (direction == -1 && Position < 0m)
				{
					BuyMarket(Math.Abs(Position));
					ResetPositionState();
				}

				break;
			}
		}

		_blackSheepTriggered = true;
	}

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

	private void UpdateTrailingLevels(ICandleMessage candle)
	{
		var trailingStop = GetPriceOffset(TrailingStopPips);

		if (trailingStop <= 0m)
		return;

		var trailingStep = GetPriceOffset(TrailingStepPips);

		if (Position > 0m)
		{
			var profit = candle.ClosePrice - _entryPrice;

			if (profit > trailingStop + trailingStep)
			{
				var candidate = candle.ClosePrice - trailingStop;

				if (_trailingLong == null || candidate > _trailingLong.Value + trailingStep)
				_trailingLong = candidate;
			}
		}
		else if (Position < 0m)
		{
			var profit = _entryPrice - candle.ClosePrice;

			if (profit > trailingStop + trailingStep)
			{
				var candidate = candle.ClosePrice + trailingStop;

				if (_trailingShort == null || candidate < _trailingShort.Value - trailingStep)
				_trailingShort = candidate;
			}
		}
	}

	private bool ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopLossPrice is decimal longSl && candle.LowPrice <= longSl)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal longTp && candle.HighPrice >= longTp)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}

			if (_trailingLong is decimal trail && candle.LowPrice <= trail)
			{
				SellMarket(Math.Abs(Position));
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			var absPosition = Math.Abs(Position);

			if (_stopLossPrice is decimal shortSl && candle.HighPrice >= shortSl)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}

			if (_takeProfitPrice is decimal shortTp && candle.LowPrice <= shortTp)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}

			if (_trailingShort is decimal trail && candle.HighPrice >= trail)
			{
				BuyMarket(absPosition);
				ResetPositionState();
				return true;
			}
		}

		return false;
	}

	private void ResetCounters()
	{
		_streakDirection = 0;
		_bullCount = 0;
		_bearCount = 0;
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_trailingLong = null;
		_trailingShort = null;
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.TimeOfDay.Hours;
		return hour >= StartHour && hour <= EndHour;
	}

	private decimal GetPriceOffset(decimal pips)
	{
		if (pips <= 0m)
		return 0m;

		return pips * _pipSize;
	}

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

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

		return 0;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
		return 1m;

		var decimals = CountDecimals(step);

		return decimals == 3 || decimals == 5
		? step * 10m
		: step;
	}

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

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

		return count;
	}
}