Auf GitHub ansehen

Kerzen-Schatten-Prozent-Strategie

Übersicht

Die Kerzen-Schatten-Prozent-Strategie ist eine direkte Portierung des MetaTrader-Expertenberaters Candle shadow percent. Sie sucht nach Kerzen, bei denen der obere oder untere Docht einen konfigurierbaren Prozentsatz des Kerzenkörpers erreicht. Wenn ein langer oberer Docht erscheint, eröffnet die Strategie eine Short-Position; wenn ein tiefer unterer Docht erscheint, eröffnet sie eine Long-Position. Die Trade-Richtung ist mit dem ursprünglichen Algorithmus abgestimmt und hält den Risikomanagement-Workflow intakt.

Konvertierungshinweise

  • Der ursprüngliche Experte hing von einem benutzerdefinierten Indikator ab. In der StockSharp-Version werden Docht- und Körperproportionen direkt aus abgeschlossenen Kerzen berechnet, daher gibt es keine externen Indikatorabhängigkeiten.
  • Pip-Werte werden aus Security.PriceStep abgeleitet. Passen Sie StopLossPips, TakeProfitPips und MinBodyPips an die Tick-Größe des Instruments an.
  • Die risikobasierte Positionsgrößenbestimmung spiegelt die MetaTrader CMoneyFixedMargin-Logik wider, indem ein Prozentsatz des aktuellen Portfolio-Werts gegen die konfigurierte Stop-Loss-Distanz riskiert wird.

Kerzen-Qualifikation

Eine Kerze wird für den Trading in Betracht gezogen, wenn:

  1. Ihre absolute Körpergröße mindestens MinBodyPips * Security.PriceStep beträgt.
  2. Der entsprechende Docht positiv ist.
  3. Das Docht-zu-Körper-Verhältnis die ausgewählte Schwellenwertlogik erfüllt:
    • Oberer Docht (Verkaufs-Setup): (High − max(Open, Close)) / Body * 100 ist größer oder gleich TopShadowPercent wenn TopShadowIsMinimum = true, andernfalls muss es kleiner oder gleich diesem Wert sein.
    • Unterer Docht (Kauf-Setup): (min(Open, Close) − Low) / Body * 100 ist größer oder gleich LowerShadowPercent wenn LowerShadowIsMinimum = true, andernfalls muss es kleiner oder gleich diesem Wert sein.
  4. Wenn beide Dochte ihre Schwellenwerte in derselben Kerze erfüllen, behält die Strategie nur die Seite mit dem größeren Dochtverhältnis, um Doppelsignale zu vermeiden.

Einstiegsregeln

  • Short-Einstieg – ausgelöst durch ein gültiges oberes Dochtsignal, während die Strategie flat oder long ist. Die Strategie kehrt bei Bedarf das bestehende Long-Engagement um und setzt sofort Schutzorders.
  • Long-Einstieg – ausgelöst durch ein gültiges unteres Dochtsignal, während die Strategie flat oder short ist. Das bestehende Short-Engagement wird automatisch geschlossen, bevor die neue Long-Position aufgebaut wird.

Ausstiegsregeln

  • Stop-Loss – platziert bei StopLossPips * Security.PriceStep vom Einstiegspreis entfernt. Long-Positionen verwenden entry − stopDistance; Short-Positionen verwenden entry + stopDistance.
  • Take-Profit – optionales Ziel bei TakeProfitPips * Security.PriceStep vom Einstieg. Wenn TakeProfitPips = 0, ist das Ziel deaktiviert und Positionen verlassen sich ausschließlich auf den Stop-Loss oder ein entgegengesetztes Signal zum Ausstieg.
  • Die Strategie überwacht abgeschlossene Kerzen. Wenn ein Kerzenbereich den Stop oder das Ziel berührt, wird die Position beim nächsten Verarbeitungszyklus geschlossen.

Positionsgrößenbestimmung

  • Das Risiko pro Trade wird als Portfolio.CurrentValue * (RiskPercent / 100) berechnet. Wenn der Portfolio-Wert nicht verfügbar ist, fällt die Strategie auf das konfigurierte Strategievolumen zurück.
  • Die Menge entspricht dem Risikobetrag dividiert durch die Stop-Loss-Distanz. Bei der Umkehr addiert der Algorithmus die absolute Größe des aktuellen Engagements, um eine vollständige Umkehr zu gewährleisten, was dem Verhalten des ursprünglichen MetaTrader-Experten entspricht.

Parameter

Parameter Beschreibung
CandleType Zeitrahmen oder Datentyp für Kerzen-Abonnements.
StopLossPips Stop-Loss-Abstand in Pips/Ticks relativ zum Instrument. Muss größer als null sein.
TakeProfitPips Take-Profit-Abstand in Pips/Ticks. Null verwenden zum Deaktivieren des Ziels.
RiskPercent Prozentualer Anteil des Portfolio-Werts, der pro Trade riskiert wird.
MinBodyPips Minimale Kerzenkörpergröße (in Pips/Ticks), die vor der Auswertung der Dochtverhältnisse erforderlich ist.
EnableTopShadow Aktiviert Short-Signale basierend auf der Länge des oberen Dochts.
TopShadowPercent Schwellenprozentsatz für das obere Docht-zu-Körper-Verhältnis.
TopShadowIsMinimum Wenn true, muss das Verhältnis größer oder gleich dem Schwellenwert sein; wenn false, muss es kleiner oder gleich sein.
EnableLowerShadow Aktiviert Long-Signale basierend auf der Länge des unteren Dochts.
LowerShadowPercent Schwellenprozentsatz für das untere Docht-zu-Körper-Verhältnis.
LowerShadowIsMinimum Steuert, ob der untere Dochtschwellenwert als Mindest- oder Höchstbedingung behandelt wird.

Verwendungstipps

  • Beginnen Sie mit einem Zeitrahmen ähnlich dem ursprünglichen EA (z. B. 5-Minuten-Kerzen) und passen Sie Pip-Abstände für Ihr Instrument an.
  • Erhöhen Sie MinBodyPips, wenn Rauschen zu viele Signale erzeugt; verringern Sie es, um kleinere Umkehrungen zu erfassen.
  • Kombinieren Sie die Strategie mit zusätzlichen Filtern (wie Trendindikatoren) durch Erweitern der Klasse—Bindungen für zusätzliche Indikatoren können innerhalb von OnStarted hinzugefügt werden.
  • Validieren Sie immer die Tick-Größeninterpretation auf einem Demo-Portfolio, bevor Sie es in der Produktion einsetzen.
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;
	}
}