Auf GitHub ansehen

Risikomanagement ATR Strategie

Überblick

Die Risikomanagement-ATR-Strategie ist eine StockSharp-Umsetzung des MetaTrader 5-Experten Risikomanagement EA basierend auf ATR Volatilität. Das ursprüngliche EA konzentrierte sich auf die automatische Größenbestimmung von Positionen entsprechend dem Kontostand und der aktuellen Marktvolatilität, gemessen durch den Average True Range (ATR). Der StockSharp-Port behält die gleiche Philosophie bei: Er eröffnet nur Long-Positionen, wenn ein einfacher gleitender 10-Perioden-Durchschnitt einen einfachen gleitenden 20-Perioden-Durchschnitt überschreitet, und jede Einstiegsgröße wird so berechnet, dass der potenzielle Verlust am Schutzstopp dem konfigurierten Risikoprozentsatz entspricht.

Die Konvertierung folgt dem übergeordneten StockSharp API. Indikatorberechnungen basieren auf den Komponenten AverageTrueRange und SimpleMovingAverage, die an das Kerzenabonnement angehängt sind, und nicht auf direkten Indikatoraufrufen. Das Handelsmanagement verwendet StockSharp-Hilfsmethoden wieder und bricht den Schutzstopp nach jeder Füllung ab und erstellt ihn neu, sodass die Nettoposition und die Stop-Order immer übereinstimmen.

Handelslogik

  1. Abonnieren Sie den durch CandleType definierten Zeitrahmen und warten Sie, bis die Kerzen vollständig geschlossen sind, um vorzeitige Entscheidungen zu vermeiden.
  2. Geben Sie einen 14-Perioden-ATR und zwei einfache gleitende Durchschnitte (Längen 10 und 20) mit den Abonnementdaten ein.
  3. Wenn der schnell gleitende Durchschnitt über dem langsam gleitenden Durchschnitt schließt und es keine offene Position gibt, berechnen Sie die Positionsgröße basierend auf dem ausgewählten Risikomodell und erteilen Sie eine Marktkauforder.
  4. Berechnen Sie nach jeder Füllung die Stop-Loss-Distanz: entweder ATR * AtrMultiplier oder eine feste Anzahl von Preisschritten, wenn UseAtrStopLoss deaktiviert ist.
  5. Runden Sie den Stop-Preis auf den nächsten Tick ab und platzieren Sie eine SellStop-Order mit der aktuellen Positionsgröße. Jeder vorherige Stopp wird gelöscht, bevor der neue registriert wird.
  6. Wenn die Stop-Order ausgeführt wird und die Position auf Null zurückkehrt, löscht die Strategie ihren internen Zustand und ist bereit für den nächsten Crossover.

Risikomanagement

  • RiskPercentage bestimmt, wie viel vom Portfoliowert bei einem einzelnen Trade verloren gehen kann. Die Strategie liest Portfolio.CurrentValue (oder BeginValue als Fallback) und multipliziert ihn mit dem Prozentsatz, um das zulässige monetäre Risiko zu erhalten.
  • Das zulässige Risiko wird durch die Stop-Loss-Distanz dividiert, um das Handelsvolumen zu erhalten. Bei der Volumenrundung werden der Volumenschritt des Instruments sowie Mindest- und Höchstbeschränkungen berücksichtigt, sodass die generierten Aufträge an der Börse gültig bleiben.
  • Wenn RiskPercentage auf 0 gesetzt ist, greift die Strategie auf die Standardeigenschaft Volume zurück (standardmäßig 1 Lot), während der automatische Schutzstopp beibehalten wird.

Parameter

Name Typ Standard Beschreibung
CandleType DataType Zeitrahmen von 1 Minute Von der Strategie verarbeitete primäre Kerzenserie.
AtrPeriod int 14 Anzahl der Kerzen, die zum Glätten des ATR-Indikators verwendet werden.
AtrMultiplier decimal 2.0 Auf den ATR-Wert angewendeter Multiplikator, um die Stop-Loss-Distanz abzuleiten.
RiskPercentage decimal 1.0 Prozentsatz des Portfoliowerts, der bei jedem Trade riskiert wird. Auf Null setzen, um ein festes Volumen zu verwenden.
UseAtrStopLoss bool true Wenn aktiviert, wird der Stopp bei ATR * AtrMultiplier platziert; andernfalls wird ein fester Abstand verwendet.
FixedStopLossPoints int 50 Anzahl der Preisschritte, die für den Schutzstopp verwendet werden, wenn die ATR-basierte Platzierung deaktiviert wird.

Unterschiede zum Original EA

  • StockSharp arbeitet mit Nettopositionen, daher übermittelt die Konvertierung nur Marktkaufaufträge. Ausstiege erfolgen durch das schützende SellStop, das das EA-Verhalten reproduziert, nach einem Stopp immer flach zu sein.
  • MetaTrader stellt die Konstante _Point für die Tick-Größe bereit. Der Port fragt Security.PriceStep ab und greift auf eine einzelne Währungseinheit zurück, wenn das Instrument keine Tick-Spezifikation bereitstellt.
  • Bei der Positionsgröße werden die Volumenfilter von StockSharp (VolumeStep, MinVolume, MaxVolume) berücksichtigt, um sicherzustellen, dass das Orderbuch die generierten Ordergrößen akzeptiert.
  • Die Indikatorverarbeitung erfolgt ereignisgesteuert über Subscription.Bind(...) statt synchroner iMA/iATR-Aufrufe.

Anwendungstipps

  • Stellen Sie sicher, dass das verbundene Portfolio einen korrekten CurrentValue meldet; andernfalls sinkt die risikobasierte Positionsgröße auf das Volumen Null zurück.
  • Die Eigenschaft Volume fungiert weiterhin als Sicherheitsnetz. Wenn Sie unabhängig von ATR-Berechnungen eine feste Losgröße wünschen, setzen Sie RiskPercentage auf Null und passen Sie Volume an, bevor Sie mit der Strategie beginnen.
  • Hängen Sie die Strategie an ein Diagramm an, um die Kerzen, sowohl gleitende Durchschnitte als auch ausgeführte Trades, zu visualisieren. Es hilft zu bestätigen, dass neue Einträge nur dann erscheinen, wenn der schnelle Durchschnitt über dem langsamen schließt und dass Stopps genau unter der letzten Preisschwankung liegen.
  • Erwägen Sie, AtrMultiplier bei volatileren Instrumenten zu erhöhen, um vorzeitige Stop-Outs zu vermeiden, oder deaktivieren Sie die ATR-basierte Platzierung und stellen Sie eine benutzerdefinierte feste Distanz durch FixedStopLossPoints bereit.

Indikatoren

  • AverageTrueRange (Länge AtrPeriod).
  • SimpleMovingAverage (schnelle Länge 10).
  • SimpleMovingAverage (langsame Länge 20).
namespace StockSharp.Samples.Strategies;

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;

public class RiskManagementAtrStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<decimal> _riskPercentage;
	private readonly StrategyParam<bool> _useAtrStopLoss;
	private readonly StrategyParam<int> _fixedStopLossPoints;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;

	private AverageTrueRange _atr;
	private SimpleMovingAverage _fastMovingAverage;
	private SimpleMovingAverage _slowMovingAverage;

	private decimal? _lastAtrValue;
	private Order _stopLossOrder;
	private decimal _priceStep;
	private decimal? _virtualStopPrice;

	public RiskManagementAtrStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe processed by the strategy.", "General");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR period", "Number of candles used to smooth the ATR volatility measure.", "Indicator");

		_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("ATR multiplier", "Distance multiplier applied to the ATR for stop-loss placement.", "Risk");

		_riskPercentage = Param(nameof(RiskPercentage), 1m)
			.SetNotNegative()
			.SetDisplay("Risk %", "Percentage of portfolio value risked on every trade.", "Risk");

		_useAtrStopLoss = Param(nameof(UseAtrStopLoss), true)
			.SetDisplay("Use ATR stop", "Switch between ATR-based and fixed-distance stop-loss modes.", "Risk");

		_fixedStopLossPoints = Param(nameof(FixedStopLossPoints), 50)
			.SetGreaterThanZero()
			.SetDisplay("Fixed stop (points)", "Stop-loss distance expressed in price steps when ATR mode is disabled.", "Risk");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA period", "Length of the fast moving average used for signals.", "Indicators");

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA period", "Length of the slow moving average used for signals.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	public decimal RiskPercentage
	{
		get => _riskPercentage.Value;
		set => _riskPercentage.Value = value;
	}

	public bool UseAtrStopLoss
	{
		get => _useAtrStopLoss.Value;
		set => _useAtrStopLoss.Value = value;
	}

	public int FixedStopLossPoints
	{
		get => _fixedStopLossPoints.Value;
		set => _fixedStopLossPoints.Value = value;
	}

	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

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

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

		_atr = null;
		_fastMovingAverage = null;
		_slowMovingAverage = null;
		_lastAtrValue = null;
		_stopLossOrder = null;
		_priceStep = 0m;
		_virtualStopPrice = null;
	}

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

		Volume = Volume > 0m ? Volume : 1m; // Provide a default lot size when no risk-based sizing is used

		_priceStep = Security?.PriceStep ?? 0m;
		if (_priceStep <= 0m)
			_priceStep = 1m; // Fallback to a single currency unit when the instrument does not expose a price step

		_atr = new AverageTrueRange
		{
			Length = AtrPeriod
		};

		_fastMovingAverage = new SimpleMovingAverage
		{
			Length = FastMaPeriod
		};

		_slowMovingAverage = new SimpleMovingAverage
		{
			Length = SlowMaPeriod
		};

		_lastAtrValue = null;
		CancelStopLossOrder();

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_atr, _fastMovingAverage, _slowMovingAverage, ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _atr);
			DrawIndicator(area, _fastMovingAverage);
			DrawIndicator(area, _slowMovingAverage);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal atrValue, decimal fastMaValue, decimal slowMaValue)
	{
		if (candle.State != CandleStates.Finished)
			return; // Work exclusively with closed candles to avoid premature entries

		_lastAtrValue = atrValue;

		// Check virtual stop-loss
		if (_virtualStopPrice.HasValue && Position > 0m && candle.LowPrice <= _virtualStopPrice.Value)
		{
			SellMarket(Math.Abs(Position));
			_virtualStopPrice = null;
			return;
		}

		if (Position == 0m)
			_virtualStopPrice = null;

		if (_atr == null || _fastMovingAverage == null || _slowMovingAverage == null)
			return;

		if (!_atr.IsFormed || !_fastMovingAverage.IsFormed || !_slowMovingAverage.IsFormed)
			return; // Ensure all indicators accumulated enough history

		if (fastMaValue <= slowMaValue)
			return; // The simple moving average crossover only buys when the fast average is above the slow one

		if (Position != 0m)
			return; // Mimic the MetaTrader expert: enter only when there is no open position

		var volume = CalculateOrderVolume(atrValue);
		if (volume <= 0m)
			return;

		CancelStopLossOrder();

		BuyMarket(volume);
	}

	private decimal CalculateOrderVolume(decimal atrValue)
	{
		var volume = Volume > 0m ? Volume : 0m;

		var stopDistance = CalculateStopDistance(atrValue);
		if (stopDistance <= 0m)
			return 0m; // Skip trading when the stop distance cannot be computed

		var riskPercent = RiskPercentage;
		if (riskPercent > 0m)
		{
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			if (portfolioValue <= 0m)
				return 0m; // Unable to size the trade without a portfolio valuation

			var riskAmount = portfolioValue * riskPercent / 100m;
			if (riskAmount <= 0m)
				return 0m;

			volume = riskAmount / stopDistance;
		}

		volume = RoundVolume(volume);
		volume = ClampVolume(volume);

		return volume > 0m ? volume : 0m;
	}

	private decimal CalculateStopDistance(decimal atrValue)
	{
		if (UseAtrStopLoss)
		{
			if (atrValue <= 0m)
				return 0m;

			var distance = atrValue * AtrMultiplier;
			return distance > 0m ? distance : 0m;
		}

		var steps = FixedStopLossPoints;
		if (steps <= 0)
			return 0m;

		return steps * _priceStep;
	}

	private decimal RoundVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0m)
				return step; // Use the minimum tradable lot when the calculated volume is below one step

			return steps * step;
		}

		return Math.Round(volume, 2, MidpointRounding.ToZero);
	}

	private decimal ClampVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var minVolume = Security?.MinVolume;
		if (minVolume != null && minVolume.Value > 0m && volume < minVolume.Value)
			volume = minVolume.Value;

		var maxVolume = Security?.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
			volume = maxVolume.Value;

		return volume;
	}

	private decimal AdjustPrice(decimal price)
	{
		if (price <= 0m)
			return 0m;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return Math.Round(price, 4, MidpointRounding.AwayFromZero);

		var steps = Math.Floor(price / step);
		if (steps <= 0m)
			return step; // Never place protective stops at non-positive prices

		return steps * step;
	}

	private void CancelStopLossOrder()
	{
		if (_stopLossOrder == null)
			return;

		if (_stopLossOrder.State == OrderStates.Active)
			CancelOrder(_stopLossOrder);

		_stopLossOrder = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order.Security != Security)
			return;

		if (Position <= 0m)
			CancelStopLossOrder();

		if (trade.Order.Side != Sides.Buy)
			return; // The expert only opens long trades; sell trades come from stop-loss execution

		var atrValue = _lastAtrValue ?? 0m;
		var stopDistance = CalculateStopDistance(atrValue);
		if (stopDistance <= 0m)
			return;

		var stopPrice = trade.Trade.Price - stopDistance;
		stopPrice = AdjustPrice(stopPrice);

		if (stopPrice <= 0m || stopPrice >= trade.Trade.Price)
			return; // Do not place invalid protective stops

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		CancelStopLossOrder();

		// Use virtual stop-loss instead of SellStop order
		_virtualStopPrice = stopPrice;
	}
}