Открыть на GitHub

Стратегия «Candle Shadow Percent»

Обзор

Candle Shadow Percent — портированная из MetaTrader экспертная система Candle shadow percent. Стратегия отслеживает свечи, у которых верхняя или нижняя тень превышает настраиваемый процент от тела. Высокая верхняя тень приводит к открытию короткой позиции, длинная нижняя тень — к длинной. Логика входов и управление рисками повторяют оригинальный советник.

Особенности конверсии

  • В исходном решении использовался внешний индикатор. В версии для StockSharp пропорции тени и тела рассчитываются напрямую по завершённым свечам, поэтому дополнительные индикаторы не требуются.
  • Значения «пунктов» вычисляются из Security.PriceStep. При необходимости подберите StopLossPips, TakeProfitPips и MinBodyPips под шаг цены инструмента.
  • Размер позиции формируется аналогично CMoneyFixedMargin: доля капитала (RiskPercent) делится на расстояние до стоп-лосса.

Фильтр свечей

Свеча участвует в анализе, если:

  1. Абсолютная величина тела не менее MinBodyPips * Security.PriceStep.
  2. Соответствующая тень положительна.
  3. Выполняется условие по проценту тени:
    • Верхняя тень (шорт): (High − max(Open, Close)) / Body * 100 сравнивается с TopShadowPercent. При TopShadowIsMinimum = true доля должна быть не меньше порога, иначе — не больше.
    • Нижняя тень (лонг): (min(Open, Close) − Low) / Body * 100 сравнивается с LowerShadowPercent по аналогичному правилу LowerShadowIsMinimum.
  4. Если обе тени удовлетворяют требованиям, выбирается направление с большим относительным размером тени — это исключает двойное срабатывание.

Правила входа

  • Шорт — при сигнале верхней тени и отсутствии короткой позиции. При наличии длинной позиции выполняется разворот и сразу выставляются уровни защиты.
  • Лонг — при сигнале нижней тени и отсутствии длинной позиции. Активная короткая позиция закрывается перед открытием новой длинной.

Правила выхода

  • Стоп-лосс: задаётся на расстоянии StopLossPips * Security.PriceStep от цены входа (ниже для лонга, выше для шорта).
  • Тейк-профит: располагается на расстоянии TakeProfitPips * Security.PriceStep. Значение 0 отключает цель и оставляет только стоп-лосс или разворот по противоположному сигналу.
  • Контроль осуществляется на закрытых свечах. Если диапазон свечи достигает стопа или цели, позиция ликвидируется при следующей обработке.

Управление размером позиции

  • Риск на сделку равен Portfolio.CurrentValue * (RiskPercent / 100). Если данные портфеля недоступны, используется стандартный объём стратегии.
  • Объём = риск / расстояние до стопа. При развороте к рассчитанному объёму добавляется величина текущей позиции, что полностью повторяет поведение оригинального советника.

Параметры

Параметр Описание
CandleType Тип/таймфрейм свечей, по которым ведётся расчёт.
StopLossPips Расстояние стоп-лосса в пунктах/тиках. Значение > 0.
TakeProfitPips Расстояние тейк-профита в пунктах/тиках. 0 — цель отключена.
RiskPercent Доля капитала, рискуемая в одной сделке.
MinBodyPips Минимальный размер тела свечи для оценки теней.
EnableTopShadow Включить сигналы по верхней тени (шорт).
TopShadowPercent Порог процента верхней тени относительно тела.
TopShadowIsMinimum Интерпретация порога: минимум (true) или максимум (false).
EnableLowerShadow Включить сигналы по нижней тени (лонг).
LowerShadowPercent Порог процента нижней тени.
LowerShadowIsMinimum Интерпретация порога нижней тени.

Рекомендации по использованию

  • Для начала используйте 5-минутные свечи, как в оригинальном советнике, и подберите параметры под ваш инструмент.
  • Увеличивайте MinBodyPips, если возникает слишком много ложных сигналов; уменьшайте, если нужно больше входов.
  • При необходимости расширяйте стратегию фильтрами тренда — дополнительные индикаторы можно привязать в методе OnStarted.
  • Перед торговлей на реальном счёте протестируйте настройки на демо, чтобы проверить корректность шага цены и управления рисками.
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;
	}
}