Ver en GitHub

Estrategia ATR Reversion

ATR Reversion busca movimientos repentinos medidos en múltiplos del Average True Range (ATR). Cuando el precio supera el multiplicador de ATR, el sistema espera una reversión a la media.

Las pruebas indican un rendimiento anual promedio de aproximadamente 133%. Funciona mejor en el mercado de criptomonedas.

La estrategia abre una operación en la dirección opuesta al impulso y utiliza una media móvil para evaluar el momentum.

Las posiciones se cierran en un cruce de media móvil o cuando se alcanza el stop de volatilidad.

Detalles

  • Criterios de entrada: El movimiento del precio supera AtrMultiplier veces ATR.
  • Largo/Corto: Ambos direcciones.
  • Criterios de salida: El precio cruza la media móvil o el stop.
  • Stops: Sí.
  • Valores predeterminados:
    • AtrPeriod = 14
    • AtrMultiplier = 2.0m
    • MAPeriod = 20
    • StopLossPercent = 2.0m
    • CandleType = TimeSpan.FromMinutes(5)
  • Filtros:
    • Categoría: Reversión a la media
    • Dirección: Ambos
    • Indicadores: ATR, MA
    • Stops: Sí
    • Complejidad: Básico
    • Marco temporal: Intradía
    • Estacionalidad: No
    • Redes neuronales: No
    • Divergencia: No
    • Nivel de riesgo: Medio
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that trades on sudden price movements measured in ATR units.
/// It enters positions when price makes a significant move in one direction (N * ATR)
/// and expects a reversion to the mean.
/// </summary>
public class AtrReversionStrategy : Strategy
{
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _stopLossPercent;

	private decimal _prevClose;
	private decimal _prevMean;
	private bool _hasPrevious;
	private Order _pendingOrder;

	/// <summary>
	/// Period for ATR calculation.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// ATR multiplier for entry signal.
	/// </summary>
	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	/// <summary>
	/// Period for Moving Average calculation for exit.
	/// </summary>
	public int MAPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Type of candles used for strategy calculation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize the ATR Reversion strategy.
	/// </summary>
	public AtrReversionStrategy()
	{
		_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
			.SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
			.SetOptimize(7, 21, 7);

		_atrMultiplier = Param(nameof(AtrMultiplier), 2.0m).SetGreaterThanZero()
			.SetDisplay("ATR Multiplier", "ATR multiplier for entry signal", "Entry")
			.SetOptimize(1.5m, 3.0m, 0.5m);

		_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
			.SetDisplay("MA Period", "Period for MA calculation for exit", "Indicators")
			.SetOptimize(10, 50, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m).SetNotNegative()
			.SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection");
		OrderRegistering += order => _pendingOrder = order;
	}

	public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevClose = default;
		_prevMean = default;
		_hasPrevious = false;
		_pendingOrder = null;
	}

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

		var atr = new AverageTrueRange { Length = AtrPeriod };
		var sma = new SimpleMovingAverage { Length = MAPeriod };
		StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
		}

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(atr, sma, ProcessCandle, false)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, sma);
			DrawIndicator(area, atr);
			DrawOwnTrades(area);
		}
	}

	private void ObserveProtectionQuote(Level1ChangeMessage quote)
	{
		// Native protection runs before the callback, including between finished candles.
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue, IIndicatorValue smaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!atrValue.IsFormed || !smaValue.IsFormed || !IsFormedAndOnlineAndAllowTrading())
			return;

		var atr = atrValue.GetValue<decimal>();
		var mean = smaValue.GetValue<decimal>();
		var close = candle.ClosePrice;
		if (!_hasPrevious)
		{
			_hasPrevious = true;
			_prevClose = close;
			_prevMean = mean;
			return;
		}

		if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
		{
			_prevClose = close;
			_prevMean = mean;
			return;
		}

		// A current level is not a crossing: compare each close against its own SMA.
		if (Position > 0m && _prevClose <= _prevMean && close > mean)
			SellMarket(Position);
		else if (Position < 0m && _prevClose >= _prevMean && close < mean)
			BuyMarket(Math.Abs(Position));
		else if (Position == 0m && atr > 0m && close - _prevClose < -AtrMultiplier * atr)
			BuyMarket(Volume);
		else if (Position == 0m && atr > 0m && close - _prevClose > AtrMultiplier * atr)
			SellMarket(Volume);
		_prevClose = close;
		_prevMean = mean;
	}
}