Ver no GitHub

Estratégia de gerenciamento de risco ATR

Visão geral

A estratégia de gerenciamento de riscos ATR é uma conversão StockSharp do MetaTrader 5 especialistas Gerenciamento de riscos EA baseado em ATR volatilidade. O EA original se concentrava no dimensionamento automático das posições de acordo com o saldo da conta e a volatilidade atual do mercado medida pelo Average True Range (ATR). A porta StockSharp mantém a mesma filosofia: ela só abre posições longas quando uma média móvel simples de 10 períodos cruza acima de uma média móvel simples de 20 períodos, e cada tamanho de entrada é calculado de modo que a perda potencial no stop de proteção corresponda à porcentagem de risco configurada.

A conversão segue o StockSharp API de alto nível. Os cálculos do indicador dependem dos componentes AverageTrueRange e SimpleMovingAverage anexados à assinatura da vela, em vez de chamadas diretas do indicador. O gerenciamento comercial reutiliza StockSharp métodos auxiliares, cancelando e recriando o stop de proteção após cada preenchimento para que a posição líquida e a ordem de stop sempre correspondam.

Lógica de negociação

  1. Assine o prazo definido por CandleType e aguarde as velas totalmente fechadas para evitar decisões prematuras.
  2. Alimente um ATR de 14 períodos e duas médias móveis simples (comprimentos 10 e 20) com os dados de assinatura.
  3. Quando a média móvel rápida fechar acima da média móvel lenta e não houver posição aberta, calcule o tamanho da posição com base no modelo de risco selecionado e envie uma ordem de compra de mercado.
  4. Após cada preenchimento, calcule a distância de stop-loss: ATR * AtrMultiplier ou um número fixo de etapas de preço quando UseAtrStopLoss estiver desativado.
  5. Arredonde o preço stop para o tick mais próximo e coloque uma ordem SellStop com o tamanho da posição atual. Qualquer parada anterior é cancelada antes que a nova seja registrada.
  6. Quando a ordem de parada é executada e a posição retorna a zero, a estratégia limpa seu estado interno, pronta para o próximo cruzamento.

Gestão de risco

  • RiskPercentage determina quanto do valor do portfólio pode ser perdido em uma única negociação. A estratégia lê Portfolio.CurrentValue (ou BeginValue como alternativa) e multiplica-o pela percentagem para obter o risco monetário permitido.
  • O risco permitido é dividido pela distância do stop loss para obter o volume de negociação. O arredondamento de volume respeita a etapa de volume do instrumento, as restrições mínimas e máximas para que as ordens geradas permaneçam válidas na bolsa.
  • Se RiskPercentage for definido como 0, a estratégia volta para a propriedade padrão Volume (1 lote por padrão) enquanto mantém a parada de proteção automática.

Parâmetros

Nome Tipo Padrão Descrição
CandleType DataType Período de 1 minuto Série de velas primárias processadas pela estratégia.
AtrPeriod int 14 Número de velas usadas para suavizar o indicador ATR.
AtrMultiplier decimal 2.0 Multiplicador aplicado ao valor ATR para derivar a distância do stop-loss.
RiskPercentage decimal 1.0 Porcentagem do valor da carteira arriscada em cada negociação. Defina como zero para usar um volume fixo.
UseAtrStopLoss bool true Quando ativado a parada é colocada em ATR * AtrMultiplier; caso contrário, uma distância fixa será usada.
FixedStopLossPoints int 50 Número de etapas de preço usadas para o stop protetor sempre que o posicionamento baseado em ATR é desativado.

Diferenças do original EA

  • StockSharp trabalha com posições líquidas, portanto a conversão apenas envia ordens de compra a mercado. As saídas acontecem através do protetor SellStop, que reproduz o comportamento EA de estar sempre flat após uma parada.
  • MetaTrader expõe a constante _Point para o tamanho do tick. A porta consulta Security.PriceStep e retorna para uma unidade monetária única quando o instrumento não fornece uma especificação de tick.
  • O dimensionamento de posição respeita os filtros de volume de StockSharp (VolumeStep, MinVolume, MaxVolume) para garantir que a carteira de pedidos aceite os tamanhos de pedidos gerados.
  • O processamento do indicador é orientado por eventos por meio de Subscription.Bind(...) em vez de chamadas síncronas iMA/iATR.

Dicas de uso

  • Certifique-se de que o portfólio conectado relate um CurrentValue correto; caso contrário, o dimensionamento da posição com base no risco voltará ao volume zero.
  • A propriedade Volume ainda atua como uma rede de segurança. Se você deseja um tamanho de lote fixo, independentemente dos cálculos de ATR, defina RiskPercentage como zero e ajuste Volume antes de iniciar a estratégia.
  • Anexe a estratégia a um gráfico para visualizar as velas, tanto as médias móveis quanto as negociações executadas. Isso ajuda a confirmar que novas entradas só aparecem quando a média rápida fecha acima da lenta e que as paradas ficam exatamente abaixo da última oscilação de preço.
  • Considere aumentar AtrMultiplier em instrumentos mais voláteis para evitar interrupções prematuras ou desative o posicionamento baseado em ATR e forneça uma distância fixa personalizada por meio de FixedStopLossPoints.

Indicadores

  • AverageTrueRange (comprimento AtrPeriod).
  • SimpleMovingAverage (comprimento rápido 10).
  • SimpleMovingAverage (comprimento lento 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;
	}
}