Ver en GitHub

Estrategia de Porcentaje de Sombra de Vela

Descripción general

La Estrategia de Porcentaje de Sombra de Vela es una portación directa del asesor experto de MetaTrader Candle shadow percent. Busca velas donde la mecha superior o inferior alcanza un porcentaje configurable del cuerpo de la vela. Cuando aparece una mecha superior alta, la estrategia abre una posición corta; cuando aparece una mecha inferior profunda, abre una posición larga. La dirección de la operación está alineada con el algoritmo original y mantiene el flujo de trabajo de gestión de riesgo intacto.

Notas de conversión

  • El experto original dependía de un indicador personalizado. En la versión de StockSharp, las proporciones de mecha y cuerpo se calculan directamente a partir de velas terminadas, por lo que no hay dependencias de indicadores externos.
  • Los valores de pip se derivan de Security.PriceStep. Ajuste StopLossPips, TakeProfitPips y MinBodyPips para que coincidan con el tamaño de tick del instrumento.
  • El dimensionamiento de posición basado en riesgo replica la lógica de CMoneyFixedMargin de MetaTrader arriesgando un porcentaje del valor actual del portafolio contra la distancia de stop-loss configurada.

Calificación de vela

Una vela se considera para trading cuando:

  1. Su tamaño de cuerpo absoluto es al menos MinBodyPips * Security.PriceStep.
  2. La mecha correspondiente es positiva.
  3. El ratio mecha-cuerpo satisface la lógica de umbral seleccionada:
    • Mecha superior (configuración de venta): (High − max(Open, Close)) / Body * 100 es mayor o igual a TopShadowPercent cuando TopShadowIsMinimum = true, de lo contrario debe ser menor o igual a ese valor.
    • Mecha inferior (configuración de compra): (min(Open, Close) − Low) / Body * 100 es mayor o igual a LowerShadowPercent cuando LowerShadowIsMinimum = true, de lo contrario debe ser menor o igual a ese valor.
  4. Cuando ambas mechas satisfacen sus umbrales en la misma vela, la estrategia mantiene solo el lado con el mayor ratio de mecha para evitar señales dobles.

Reglas de entrada

  • Entrada corta – activada por una señal de mecha superior válida mientras la estrategia está plana o larga. La estrategia invierte la exposición larga existente si es necesario y establece las órdenes de protección inmediatamente.
  • Entrada larga – activada por una señal de mecha inferior válida mientras la estrategia está plana o corta. La exposición corta existente se cierra automáticamente antes de establecer la nueva posición larga.

Reglas de salida

  • Stop-loss – colocado a StopLossPips * Security.PriceStep del precio de entrada. Las posiciones largas usan entrada − distanciaStop; las posiciones cortas usan entrada + distanciaStop.
  • Take-profit – objetivo opcional ubicado a TakeProfitPips * Security.PriceStep de la entrada. Cuando TakeProfitPips = 0, el objetivo está deshabilitado y las posiciones dependen únicamente del stop-loss o señal opuesta para salir.
  • La estrategia monitorea velas completadas. Si un rango de vela toca el stop o el objetivo, la posición se cierra en el siguiente ciclo de procesamiento.

Dimensionamiento de posición

  • El riesgo por operación se calcula como Portfolio.CurrentValue * (RiskPercent / 100). Si el valor del portafolio no está disponible, la estrategia recurre al volumen de estrategia configurado.
  • La cantidad es igual al monto de riesgo dividido por la distancia del stop-loss. Al invertir, el algoritmo agrega el tamaño absoluto de la exposición actual para asegurar una inversión completa, coincidiendo con el comportamiento del experto original de MetaTrader.

Parámetros

Parámetro Descripción
CandleType Marco temporal o tipo de datos usado para las suscripciones de velas.
StopLossPips Distancia del stop-loss expresada en pips/ticks relativa al instrumento. Debe ser mayor que cero.
TakeProfitPips Distancia del take-profit en pips/ticks. Usar cero para deshabilitar el objetivo.
RiskPercent Porcentaje del valor del portafolio arriesgado por operación.
MinBodyPips Tamaño mínimo del cuerpo de la vela (en pips/ticks) requerido antes de evaluar los ratios de mecha.
EnableTopShadow Habilita señales cortas basadas en la longitud de la mecha superior.
TopShadowPercent Porcentaje umbral para el ratio mecha superior-cuerpo.
TopShadowIsMinimum Cuando es true, el ratio debe ser mayor o igual al umbral; cuando es false, debe ser menor o igual a él.
EnableLowerShadow Habilita señales largas basadas en la longitud de la mecha inferior.
LowerShadowPercent Porcentaje umbral para el ratio mecha inferior-cuerpo.
LowerShadowIsMinimum Controla si el umbral de la mecha inferior se trata como condición mínima o máxima.

Consejos de uso

  • Comience con un marco temporal similar al EA original (p. ej., velas de 5 minutos) y ajuste las distancias en pips para su instrumento.
  • Aumente MinBodyPips si el ruido produce demasiadas señales; disminúyalo para capturar reversiones más pequeñas.
  • Combine la estrategia con filtros adicionales (como indicadores de tendencia) extendiendo la clase—las vinculaciones para indicadores adicionales se pueden agregar dentro de OnStarted.
  • Siempre valide la interpretación del tamaño de tick en un portafolio demo antes de desplegarlo en producción.
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;
	}
}