Ver en GitHub

Estrategia de gestión de riesgos ATR

Descripción general

La estrategia de Gestión de riesgos ATR es una conversión StockSharp de los MetaTrader 5 expertos Gestión de riesgos EA basada en la volatilidad ATR. El EA original se centró en dimensionar automáticamente las posiciones de acuerdo con el saldo de la cuenta y la volatilidad actual del mercado medida por el rango verdadero promedio (ATR). El puerto StockSharp mantiene la misma filosofía: solo abre posiciones largas cuando una media móvil simple de 10 períodos cruza por encima de una media móvil simple de 20 períodos, y cada tamaño de entrada se calcula de modo que la pérdida potencial en el stop de protección coincida con el porcentaje de riesgo configurado.

La conversión sigue el nivel alto StockSharp API. Los cálculos de los indicadores se basan en los componentes AverageTrueRange y SimpleMovingAverage adjuntos a la suscripción de velas en lugar de llamadas directas al indicador. La gestión comercial reutiliza StockSharp métodos auxiliares, cancelando y recreando la parada de protección después de cada ejecución para que la posición neta y la orden de parada siempre coincidan.

Lógica comercial

  1. Suscríbase al período de tiempo definido por CandleType y espere a que las velas se cierren por completo para evitar decisiones prematuras.
  2. Alimente un ATR de 14 períodos y dos promedios móviles simples (longitudes 10 y 20) con los datos de suscripción.
  3. Cuando la media móvil rápida cierra por encima de la media móvil lenta y no hay ninguna posición abierta, calcule el tamaño de la posición según el modelo de riesgo seleccionado y envíe una orden de compra de mercado.
  4. Después de cada llenado, calcule la distancia del límite de pérdidas: ya sea ATR * AtrMultiplier o un número fijo de pasos de precio cuando UseAtrStopLoss está deshabilitado.
  5. Redondee el precio de parada al tick más cercano y coloque una orden SellStop con el tamaño de posición actual. Cualquier parada anterior se cancela antes de que se registre la nueva.
  6. Cuando se ejecuta la orden de parada y la posición vuelve a cero, la estrategia borra su estado interno, lista para el próximo cruce.

Gestión de riesgos

  • RiskPercentage determina cuánto del valor de la cartera se puede perder en una sola operación. La estrategia lee Portfolio.CurrentValue (o BeginValue como alternativa) y lo multiplica por el porcentaje para obtener el riesgo monetario permitido.
  • El riesgo permitido se divide por la distancia del stop-loss para obtener el volumen comercial. El redondeo de volumen respeta el paso de volumen del instrumento y las restricciones mínimas y máximas para que las órdenes generadas sigan siendo válidas en la bolsa.
  • Si RiskPercentage se establece en 0, la estrategia vuelve a la propiedad predeterminada Volume (1 lote por defecto) mientras mantiene la parada de protección automática.

Parámetros

Nombre Tipo Predeterminado Descripción
CandleType DataType plazo de 1 minuto Serie de velas primarias procesadas por la estrategia.
AtrPeriod int 14 Número de velas utilizadas para suavizar el indicador ATR.
AtrMultiplier decimal 2.0 Multiplicador aplicado al valor ATR para derivar la distancia de stop-loss.
RiskPercentage decimal 1.0 Porcentaje del valor de la cartera arriesgado en cada operación. Establezca en cero para utilizar un volumen fijo.
UseAtrStopLoss bool true Cuando está habilitado, la parada se coloca en ATR * AtrMultiplier; de lo contrario se utiliza una distancia fija.
FixedStopLossPoints int 50 Número de pasos de precio utilizados para la parada de protección siempre que se deshabilite la ubicación basada en ATR.

Diferencias con el EA original

  • StockSharp funciona con posiciones netas, por lo tanto, la conversión solo envía órdenes de compra de mercado. Las salidas se realizan a través de la protección SellStop, que reproduce el comportamiento EA de estar siempre plano después de una parada.
  • MetaTrader expone la constante _Point para el tamaño del tick. El puerto consulta Security.PriceStep y recurre a una única unidad monetaria cuando el instrumento no proporciona una especificación de tick.
  • El tamaño de la posición respeta los filtros de volumen de StockSharp (VolumeStep, MinVolume, MaxVolume) para garantizar que el libro de pedidos acepte los tamaños de pedidos generados.
  • El procesamiento del indicador se basa en eventos a través de Subscription.Bind(...) en lugar de llamadas iMA/iATR sincrónicas.

Consejos de uso

  • Asegúrese de que la cartera conectada informe un CurrentValue correcto; de lo contrario, el tamaño de la posición basado en el riesgo volverá a caer a volumen cero.
  • La propiedad Volume todavía actúa como una red de seguridad. Si desea un tamaño de lote fijo independientemente de los cálculos de ATR, establezca RiskPercentage en cero y ajuste Volume antes de comenzar la estrategia.
  • Adjunte la estrategia a un gráfico para visualizar las velas, tanto las medias móviles como las operaciones ejecutadas. Ayuda a confirmar que las nuevas entradas solo aparecen cuando el promedio rápido cierra por encima del lento y que las paradas se ubican exactamente por debajo de la última oscilación del precio.
  • Considere aumentar AtrMultiplier en instrumentos más volátiles para evitar paradas prematuras, o deshabilite la ubicación basada en ATR y proporcione una distancia fija personalizada a través de FixedStopLossPoints.

Indicadores

  • AverageTrueRange (longitud AtrPeriod).
  • SimpleMovingAverage (longitud rápida 10).
  • SimpleMovingAverage (longitud lenta 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;
	}
}