Ver no GitHub

Estratégia de Percentual de Sombra de Vela

Visão geral

A Estratégia de Percentual de Sombra de Vela é uma portagem direta do consultor especialista do MetaTrader Candle shadow percent. Ela procura velas onde a sombra superior ou inferior atinge uma porcentagem configurável do corpo da vela. Quando uma sombra superior longa aparece, a estratégia abre uma posição vendida; quando uma sombra inferior profunda aparece, abre uma posição comprada. A direção da operação está alinhada com o algoritmo original e mantém o fluxo de trabalho de gestão de risco intacto.

Notas de conversão

  • O especialista original dependia de um indicador personalizado. Na versão do StockSharp, as proporções de sombra e corpo são calculadas diretamente a partir de velas terminadas, portanto não há dependências de indicadores externos.
  • Os valores de pip são derivados de Security.PriceStep. Ajuste StopLossPips, TakeProfitPips e MinBodyPips para corresponder ao tamanho do tick do instrumento.
  • O dimensionamento de posição baseado em risco replica a lógica CMoneyFixedMargin do MetaTrader ao arriscar uma porcentagem do valor atual do portfólio contra a distância de stop-loss configurada.

Qualificação de vela

Uma vela é considerada para trading quando:

  1. Seu tamanho de corpo absoluto é pelo menos MinBodyPips * Security.PriceStep.
  2. A sombra correspondente é positiva.
  3. O ratio sombra-corpo satisfaz a lógica de limiar selecionada:
    • Sombra superior (configuração de venda): (High − max(Open, Close)) / Body * 100 é maior ou igual a TopShadowPercent quando TopShadowIsMinimum = true, caso contrário deve ser menor ou igual a esse valor.
    • Sombra inferior (configuração de compra): (min(Open, Close) − Low) / Body * 100 é maior ou igual a LowerShadowPercent quando LowerShadowIsMinimum = true, caso contrário deve ser menor ou igual a esse valor.
  4. Quando ambas as sombras satisfazem seus limiares na mesma vela, a estratégia mantém apenas o lado com o maior ratio de sombra para evitar sinais duplos.

Regras de entrada

  • Entrada vendida – acionada por um sinal de sombra superior válido enquanto a estratégia está plana ou comprada. A estratégia reverte a exposição comprada existente se necessário e estabelece as ordens de proteção imediatamente.
  • Entrada comprada – acionada por um sinal de sombra inferior válido enquanto a estratégia está plana ou vendida. A exposição vendida existente é fechada automaticamente antes de estabelecer a nova posição comprada.

Regras de saída

  • Stop-loss – colocado a StopLossPips * Security.PriceStep do preço de entrada. Posições compradas usam entrada − distânciaStop; posições vendidas usam entrada + distânciaStop.
  • Take-profit – alvo opcional localizado a TakeProfitPips * Security.PriceStep da entrada. Quando TakeProfitPips = 0, o alvo está desabilitado e as posições dependem exclusivamente do stop-loss ou sinal oposto para sair.
  • A estratégia monitora velas concluídas. Se um intervalo de vela tocar o stop ou o alvo, a posição é fechada no próximo ciclo de processamento.

Dimensionamento de posição

  • O risco por operação é calculado como Portfolio.CurrentValue * (RiskPercent / 100). Se o valor do portfólio não estiver disponível, a estratégia recorre ao volume de estratégia configurado.
  • A quantidade é igual ao valor de risco dividido pela distância do stop-loss. Ao reverter, o algoritmo adiciona o tamanho absoluto da exposição atual para garantir uma reversão completa, correspondendo ao comportamento do especialista original do MetaTrader.

Parâmetros

Parâmetro Descrição
CandleType Período ou tipo de dados usado para subscrições de velas.
StopLossPips Distância do stop-loss expressa em pips/ticks relativa ao instrumento. Deve ser maior que zero.
TakeProfitPips Distância do take-profit em pips/ticks. Usar zero para desabilitar o alvo.
RiskPercent Porcentagem do valor do portfólio arriscada por operação.
MinBodyPips Tamanho mínimo do corpo da vela (em pips/ticks) necessário antes de avaliar os ratios de sombra.
EnableTopShadow Habilita sinais vendidos baseados no comprimento da sombra superior.
TopShadowPercent Porcentagem limiar para o ratio sombra superior-corpo.
TopShadowIsMinimum Quando true, o ratio deve ser maior ou igual ao limiar; quando false, deve ser menor ou igual a ele.
EnableLowerShadow Habilita sinais comprados baseados no comprimento da sombra inferior.
LowerShadowPercent Porcentagem limiar para o ratio sombra inferior-corpo.
LowerShadowIsMinimum Controla se o limiar de sombra inferior é tratado como condição mínima ou máxima.

Dicas de uso

  • Comece com um período similar ao EA original (p. ex., velas de 5 minutos) e ajuste as distâncias em pips para o seu instrumento.
  • Aumente MinBodyPips se o ruído produz muitos sinais; diminua para capturar reversões menores.
  • Combine a estratégia com filtros adicionais (como indicadores de tendência) estendendo a classe—vinculações para indicadores adicionais podem ser adicionadas dentro de OnStarted.
  • Sempre valide a interpretação do tamanho do tick em um portfólio demo antes de implantar em produção.
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>
/// Candle shadow percent strategy converted from MetaTrader.
/// Trades when a candle shows an extended wick compared to its body.
/// Position size is derived from risk percentage and stop distance.
/// </summary>
public class CandleShadowPercentStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _minBodyPips;
	private readonly StrategyParam<bool> _enableTopShadow;
	private readonly StrategyParam<decimal> _topShadowPercent;
	private readonly StrategyParam<bool> _topShadowIsMinimum;
	private readonly StrategyParam<bool> _enableLowerShadow;
	private readonly StrategyParam<decimal> _lowerShadowPercent;
	private readonly StrategyParam<bool> _lowerShadowIsMinimum;
	private readonly StrategyParam<DataType> _candleType;
	
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;
	private decimal? _entryPrice;
	
	/// <summary>
	/// Stop loss in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}
	
	/// <summary>
	/// Take profit in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}
	
	/// <summary>
	/// Risk percentage per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}
	
	/// <summary>
	/// Minimum body size in pips to evaluate shadows.
	/// </summary>
	public int MinBodyPips
	{
		get => _minBodyPips.Value;
		set => _minBodyPips.Value = value;
	}
	
	/// <summary>
	/// Enables signals based on the top shadow.
	/// </summary>
	public bool EnableTopShadow
	{
		get => _enableTopShadow.Value;
		set => _enableTopShadow.Value = value;
	}
	
	/// <summary>
	/// Threshold for the top shadow as a percentage of the body.
	/// </summary>
	public decimal TopShadowPercent
	{
		get => _topShadowPercent.Value;
		set => _topShadowPercent.Value = value;
	}
	
	/// <summary>
	/// If true the top shadow percentage acts as a minimum threshold.
	/// </summary>
	public bool TopShadowIsMinimum
	{
		get => _topShadowIsMinimum.Value;
		set => _topShadowIsMinimum.Value = value;
	}
	
	/// <summary>
	/// Enables signals based on the lower shadow.
	/// </summary>
	public bool EnableLowerShadow
	{
		get => _enableLowerShadow.Value;
		set => _enableLowerShadow.Value = value;
	}
	
	/// <summary>
	/// Threshold for the lower shadow as a percentage of the body.
	/// </summary>
	public decimal LowerShadowPercent
	{
		get => _lowerShadowPercent.Value;
		set => _lowerShadowPercent.Value = value;
	}
	
	/// <summary>
	/// If true the lower shadow percentage acts as a minimum threshold.
	/// </summary>
	public bool LowerShadowIsMinimum
	{
		get => _lowerShadowIsMinimum.Value;
		set => _lowerShadowIsMinimum.Value = value;
	}
	
	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}
	
	/// <summary>
	/// Initializes a new instance of the <see cref="CandleShadowPercentStrategy"/>.
	/// </summary>
	public CandleShadowPercentStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss", "Stop loss distance in pips", "Risk")
			;
		
		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take profit distance in pips", "Risk")
			;
		
		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Risk percentage per trade", "Risk")
			;
		
		_minBodyPips = Param(nameof(MinBodyPips), 300)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Body", "Minimum candle body size in pips", "Pattern")
			;
		
		_enableTopShadow = Param(nameof(EnableTopShadow), true)
			.SetDisplay("Use Top Shadow", "Enable sell signals from upper wicks", "Pattern");
		
		_topShadowPercent = Param(nameof(TopShadowPercent), 30m)
			.SetNotNegative()
			.SetDisplay("Top Shadow %", "Upper wick percentage threshold", "Pattern")
			;
		
		_topShadowIsMinimum = Param(nameof(TopShadowIsMinimum), true)
			.SetDisplay("Top Shadow Uses Min", "If true the threshold is treated as a minimum", "Pattern");
		
		_enableLowerShadow = Param(nameof(EnableLowerShadow), true)
			.SetDisplay("Use Lower Shadow", "Enable buy signals from lower wicks", "Pattern");
		
		_lowerShadowPercent = Param(nameof(LowerShadowPercent), 80m)
			.SetNotNegative()
			.SetDisplay("Lower Shadow %", "Lower wick percentage threshold", "Pattern")
			;
		
		_lowerShadowIsMinimum = Param(nameof(LowerShadowIsMinimum), true)
			.SetDisplay("Lower Shadow Uses Min", "If true the threshold is treated as a minimum", "Pattern");
		
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for pattern detection", "Data");
	}
	
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}
	
	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		_entryPrice = null;
	}
	
	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
		
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}
	
	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;
		
		ManageOpenPosition(candle);
		
		var pipSize = GetPipSize();
		var minBody = MinBodyPips * pipSize;
		
		var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		if (body < minBody || body <= 0m)
			return;
		
		var upperShadow = candle.HighPrice - Math.Max(candle.OpenPrice, candle.ClosePrice);
		var lowerShadow = Math.Min(candle.OpenPrice, candle.ClosePrice) - candle.LowPrice;
		
		var topRatio = body > 0m ? upperShadow / body * 100m : 0m;
		var lowerRatio = body > 0m ? lowerShadow / body * 100m : 0m;
		
		var topSignal = EnableTopShadow && upperShadow > 0m && CheckThreshold(topRatio, TopShadowPercent, TopShadowIsMinimum);
		var lowerSignal = EnableLowerShadow && lowerShadow > 0m && CheckThreshold(lowerRatio, LowerShadowPercent, LowerShadowIsMinimum);
		
		if (topSignal && lowerSignal)
		{
			if (topRatio > lowerRatio)
				lowerSignal = false;
			else
				topSignal = false;
		}
		
		if (topSignal && Position <= 0)
		{
			EnterShort(candle, pipSize);
		}
		else if (lowerSignal && Position >= 0)
		{
			EnterLong(candle, pipSize);
		}
	}
	
	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var stopHit = _longStop.HasValue && candle.LowPrice <= _longStop.Value;
			var takeHit = _longTake.HasValue && candle.HighPrice >= _longTake.Value;
			
			if (stopHit || takeHit)
			{
				SellMarket();
				this.LogInfo($"Closing long at {candle.ClosePrice}. Stop hit: {stopHit}, Take hit: {takeHit}");
				_longStop = null;
				_longTake = null;
				_entryPrice = null;
			}
		}
		else if (Position < 0)
		{
			var stopHit = _shortStop.HasValue && candle.HighPrice >= _shortStop.Value;
			var takeHit = _shortTake.HasValue && candle.LowPrice <= _shortTake.Value;
			
			if (stopHit || takeHit)
			{
				BuyMarket();
				this.LogInfo($"Closing short at {candle.ClosePrice}. Stop hit: {stopHit}, Take hit: {takeHit}");
				_shortStop = null;
				_shortTake = null;
				_entryPrice = null;
			}
		}
	}
	
	private void EnterLong(ICandleMessage candle, decimal pipSize)
	{
		var stopDistance = StopLossPips * pipSize;
		if (stopDistance <= 0m)
			return;

		var takeDistance = TakeProfitPips * pipSize;

		var entryPrice = candle.ClosePrice;
		var stopPrice = entryPrice - stopDistance;
		var takePrice = takeDistance > 0m ? entryPrice + takeDistance : (decimal?)null;

		BuyMarket();

		_longStop = stopPrice;
		_longTake = takePrice;
		_shortStop = null;
		_shortTake = null;
		_entryPrice = entryPrice;

		this.LogInfo($"Entered long at {entryPrice}. Stop {stopPrice}, Take {(takePrice.HasValue ? takePrice.Value.ToString() : "n/a")}");
	}
	
	private void EnterShort(ICandleMessage candle, decimal pipSize)
	{
		var stopDistance = StopLossPips * pipSize;
		if (stopDistance <= 0m)
			return;

		var takeDistance = TakeProfitPips * pipSize;

		var entryPrice = candle.ClosePrice;
		var stopPrice = entryPrice + stopDistance;
		var takePrice = takeDistance > 0m ? entryPrice - takeDistance : (decimal?)null;

		SellMarket();

		_shortStop = stopPrice;
		_shortTake = takePrice;
		_longStop = null;
		_longTake = null;
		_entryPrice = entryPrice;

		this.LogInfo($"Entered short at {entryPrice}. Stop {stopPrice}, Take {(takePrice.HasValue ? takePrice.Value.ToString() : "n/a")}");
	}
	
	private decimal CalculatePositionSize(decimal stopDistance)
	{
		var defaultVolume = Volume > 0m ? Volume : 1m;
		
		var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (portfolioValue <= 0m)
			return defaultVolume;

		var riskAmount = portfolioValue * (RiskPercent / 100m);
		if (riskAmount <= 0m || stopDistance <= 0m)
			return defaultVolume;
		
		var size = riskAmount / stopDistance;
		return size > 0m ? size : defaultVolume;
	}
	
	private static bool CheckThreshold(decimal ratio, decimal threshold, bool isMinimum)
	{
		return isMinimum ? ratio >= threshold : ratio <= threshold;
	}
	
	private decimal GetPipSize()
	{
		return Security?.PriceStep ?? 1m;
	}
}