Auf GitHub ansehen

NUp1Down-Strategie

Übersicht

Die NUp1Down-Strategie ist eine direkte Konvertierung des MetaTrader 5-Experten "N bars up, then one bar down" (Datei NUp1Down.mq5). Sie scannt abgeschlossene Kerzen von StockSharp und eröffnet eine Short-Position, wenn eine Baisse-Kerze nach einer konfigurierbaren Sequenz von Hausse-Kerzen erscheint, die immer höhere Schlusspreise erzielen. Die Strategie ist für diskretionäre Trader konzipiert, die ein klassisches Swing-Umkehrmuster innerhalb von StockSharp Designer, Shell oder Runner automatisieren möchten.

Handelslogik

  1. Nur auf abgeschlossenen Kerzen arbeiten, die durch den Parameter CandleType bereitgestellt werden.
  2. Die letzten BarsCount + 1 Kerzen im Speicher halten. Die neueste Kerze muss unter ihrer Eröffnung schließen (bärische Setup-Kerze).
  3. Die vorherigen BarsCount Kerzen müssen alle über ihren Eröffnungen schließen. Jede dieser Hausse-Kerzen (außer der ältesten) muss auch über dem Schlusskurs der unmittelbar vorangegangenen Kerze schließen, was eine "Treppenstufen"-Bewegung nach oben erzwingt.
  4. Wenn das Muster validiert und keine aktive Short-Position vorhanden ist, sendet die Strategie eine Marktverkaufsorder.
  5. Die Positionsgrößenbestimmung verwendet den Parameter RiskPercent. Der Algorithmus schätzt, wie viele Kontrakte eröffnet werden können, so dass das Risikokapital (Abstand zum Stop-Loss in Geldwert umgerechnet) den gewählten Prozentsatz des Portfolios nicht überschreitet. Die Basis-Volume-Eigenschaft bleibt die Mindestlotgröße und das Risikomodell kann die Handelsgröße nur erhöhen.

Positionsverwaltung

  • Beim Einstieg werden ein schützender Stop-Loss und ein Take-Profit-Level vom Einstiegspreis berechnet. Beide Abstände werden in Pips ausgedrückt und mit dem PriceStep des Instruments in Preise übersetzt. Für Symbole mit drei oder fünf Dezimalstellen wird die Pip-Größe automatisch angepasst, um der Pip-Definition von MetaTrader zu entsprechen.
  • Ein Trailing-Stop wird bei jeder abgeschlossenen Kerze neu berechnet. Der Trailing-Abstand entspricht TrailingStopPips und der Stop wird nur verschoben, wenn sich der Preis mindestens TrailingStepPips zugunsten des Handels bewegt hat. Die Trailing-Logik emuliert den ursprünglichen Experten: Bei Short-Trades folgt er dem Ask-Preis nach unten, während Long-Trades von dieser Strategie nicht generiert werden.
  • Ausstiegsbedingungen werden vor der Suche nach neuen Einstiegen bei jeder Kerze bewertet. Die Strategie schließt die Position, wenn entweder der Stop-Loss oder Take-Profit getroffen wird, oder wenn die Trailing-Logik den Stop über den aktuellen Ask-Preis strafft.

Parameter

Name Beschreibung
BarsCount Anzahl der Hausse-Kerzen vor der bärischen Setup-Kerze (Standard: 3).
TakeProfitPips Take-Profit-Abstand in Pips, angewendet auf den Einstiegspreis (Standard: 50).
StopLossPips Stop-Loss-Abstand in Pips, angewendet auf den Einstiegspreis (Standard: 50).
TrailingStopPips Abstand zwischen Marktpreis und Trailing-Stop (Standard: 10).
TrailingStepPips Minimale günstige Bewegung, bevor der Trailing-Stop vorrückt (Standard: 5).
RiskPercent Prozentualer Anteil des Portfoliokapitals, der pro Trade riskiert wird (Standard: 5).
CandleType Kerzen-Datentyp/Zeitrahmen für die Mustererkennung (Standard: 1 Stunde).

Verwendungshinweise

  • Konfigurieren Sie die Volume-Eigenschaft auf die von Ihrem Broker erlaubte Mindestordergröße. Die risikobasierte Größenbestimmung kann die Handelsgröße erhöhen, reduziert sie aber nie unter Volume.
  • Die Strategie hält jederzeit nur eine aggregierte Short-Position. Wenn eine Long-Position vorhanden ist, wird diese vor dem Eröffnen der Short-Position geschlossen.
  • Der Algorithmus arbeitet mit Kerzendaten. Intrabar-Stop-Loss- oder Take-Profit-Treffer werden mit dem Kerzenhoch/-tief erkannt, sodass der tatsächliche Ausführungszeitpunkt vom Tick-Level-Execution abweichen kann.
  • In dieser Version wird keine Python-Version bereitgestellt. Nur die C#-Implementierung in API/2574/CS/NUp1DownStrategy.cs ist verfügbar.
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;

using System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that sells after a sequence of bullish candles followed by a bearish candle.
/// </summary>
public class NUp1DownStrategy : Strategy
{
	private readonly StrategyParam<int> _barsCount;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<(decimal Open, decimal Close)> _recentCandles = new();

	private decimal _pipSize;
	private decimal? _entryPrice;
	private decimal? _activeStopPrice;
	private decimal? _activeTakePrice;

	/// <summary>
	/// Number of consecutive bullish bars required before the bearish setup candle.
	/// </summary>
	public int BarsCount
	{
		get => _barsCount.Value;
		set => _barsCount.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing stop step in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Risk percentage used to size the position.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Candle type used for pattern detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="NUp1DownStrategy"/> class.
	/// </summary>
	public NUp1DownStrategy()
	{
		Volume = 1m;

		_barsCount = Param(nameof(BarsCount), 3)
			.SetGreaterThanZero()
			.SetDisplay("Bullish Bars", "Number of bullish bars before the down bar", "General")
			
			.SetOptimize(2, 6, 1);

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
			
			.SetOptimize(20m, 120m, 10m);

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
			
			.SetOptimize(20m, 120m, 10m);

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk")
			
			.SetOptimize(5m, 30m, 5m);

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Step (pips)", "Trailing step before adjusting stop", "Risk")
			
			.SetOptimize(1m, 20m, 1m);

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Portfolio risk percentage per trade", "Money Management")
			
			.SetOptimize(1m, 10m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for candle analysis", "General");
	}

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

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

		_recentCandles.Clear();
		_pipSize = 0m;
		ResetPositionState();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = CalculatePipSize();

		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;

		UpdateTrailingAndExits(candle);

		_recentCandles.Enqueue((candle.OpenPrice, candle.ClosePrice));
		while (_recentCandles.Count > BarsCount + 1)
			_recentCandles.Dequeue();

		if (_recentCandles.Count < BarsCount + 1)
			return;

		var candles = _recentCandles.ToArray();
		var last = candles[^1];

		if (last.Close >= last.Open)
			return;

		var isPattern = true;

		for (var i = 1; i <= BarsCount; i++)
		{
			var index = candles.Length - 1 - i;
			var bar = candles[index];

			if (bar.Close <= bar.Open)
			{
				isPattern = false;
				break;
			}

			if (i < BarsCount)
			{
				var prev = candles[index - 1];
				if (bar.Close <= prev.Close)
				{
					isPattern = false;
					break;
				}
			}
		}

		if (!isPattern)
			return;

		if (Position < 0)
			return;

		SellMarket();

		_entryPrice = candle.ClosePrice;
		_activeStopPrice = _entryPrice + StopLossPips * _pipSize;
		_activeTakePrice = _entryPrice - TakeProfitPips * _pipSize;

		this.LogInfo($"Short entry after {BarsCount} bullish bars at {_entryPrice:0.#####}");
	}

	private void UpdateTrailingAndExits(ICandleMessage candle)
	{
		if (Position < 0)
		{
			var volumeToClose = Math.Abs(Position);
			if (volumeToClose <= 0m)
				return;

			if (_activeStopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				this.LogInfo($"Short exit by stop-loss at {stop:0.#####}");
				ResetPositionState();
				return;
			}

			if (_activeTakePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				this.LogInfo($"Short exit by take-profit at {take:0.#####}");
				ResetPositionState();
				return;
			}

			if (_activeStopPrice is decimal trailingStop)
			{
				var trailingDistance = TrailingStopPips * _pipSize;
				var trailingStep = TrailingStepPips * _pipSize;

				if (trailingDistance <= 0m)
					return;

				var currentAsk = candle.ClosePrice;
				var newStopCandidate = currentAsk + trailingDistance;

				if (newStopCandidate + trailingStep < trailingStop)
				{
					_activeStopPrice = newStopCandidate;
					this.LogInfo($"Short trailing stop moved to {_activeStopPrice:0.#####}");
				}
			}
		}
		else if (Position == 0)
		{
			ResetPositionState();
		}
	}

	private decimal CalculatePipSize()
	{
		if (Security?.PriceStep is decimal step && step > 0m)
		{
			var decimals = CountDecimalPlaces(step);
			return decimals is 3 or 5 ? step * 10m : step;
		}

		return 1m;
	}

	private static int CountDecimalPlaces(decimal value)
	{
		var text = value.ToString(CultureInfo.InvariantCulture);
		var separatorIndex = text.IndexOf('.');
		return separatorIndex >= 0 ? text.Length - separatorIndex - 1 : 0;
	}

	private decimal CalculateOrderVolume()
	{
		var baseVolume = Volume;
		var stopDistance = StopLossPips * _pipSize;

		if (Portfolio == null || stopDistance <= 0m)
			return baseVolume;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m)
			return baseVolume;

		var capital = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (capital <= 0m)
			return baseVolume;

		var riskAmount = capital * (RiskPercent / 100m);
		if (riskAmount <= 0m)
			return baseVolume;

		var riskPerUnit = stopDistance;
		if (riskPerUnit <= 0m)
			return baseVolume;

		var volumeFromRisk = riskAmount / riskPerUnit;
		if (volumeFromRisk <= 0m)
			return baseVolume;

		return Math.Max(baseVolume, volumeFromRisk);
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_activeStopPrice = null;
		_activeTakePrice = null;
	}
}