Ver en GitHub

Estrategia de reversión a la media Donchian

Descripción general

Esta estrategia es una adaptación del MetaTrader asesor experto MeanReversion.mq5. Opera con un patrón simple de reversión a la media: cada vez que el precio registra un nuevo mínimo dentro de la ventana retrospectiva seleccionada, la estrategia abre una posición larga, apuntando al punto medio del rango reciente. Cuando aparece un nuevo máximo, la estrategia refleja la lógica del lado corto. El tamaño de la posición se calcula a partir del porcentaje de riesgo y la distancia de parada, replicando fielmente el cálculo del lote que realiza el EA original.

Lógica de trading

  1. Cree un canal Donchian utilizando el tipo de vela configurado y el período retrospectivo. La banda superior marca el máximo más alto y la banda inferior el mínimo más bajo sobre la ventana. El punto medio (upper + lower) / 2 actúa como objetivo de reversión a la media.
  2. Si la vela finalizada actual alcanza un nuevo mínimo (Low <= LowerBand) y no hay ninguna posición abierta, la estrategia compra en el mercado. La parada de protección se refleja alrededor del precio de entrada, de modo que el punto medio se convierte en el objetivo de ganancias, coincidiendo con el cálculo MetaTrader sl = 2 * Ask - tp.
  3. Si la vela alcanza un nuevo máximo (High >= UpperBand) y no hay ninguna posición abierta, la estrategia vende en el mercado con un stop simétrico por encima del precio. El punto medio vuelve a actuar como nivel de obtención de beneficios.
  4. El stop-loss y el take-profit se controlan en cada vela terminada. Una ruptura más allá del stop cierra la posición inmediatamente, mientras que al tocar el punto medio se sale de la operación en el objetivo previsto. El estado interno se reinicia automáticamente cada vez que la posición es plana.

Dimensionamiento de posiciones

  • El riesgo por operación es igual a Portfolio.CurrentValue * (RiskPercent / 100). Si los datos de la cartera no están disponibles, la estrategia vuelve al volumen mínimo negociable.
  • El riesgo del contrato se mide como |EntryPrice - StopPrice|. El volumen bruto es RiskAmount / perUnitRisk y está normalizado al paso de volumen del instrumento. Se respetan las restricciones cambiarias mínimas y máximas. Cuando el volumen normalizado es menor que el tamaño mínimo comercializable, se utiliza el mínimo.

Parámetros

Nombre Descripción Predeterminado
CandleType Tipo de vela y período de tiempo utilizados para crear el canal Donchian. plazo de 15 minutos
LookbackPeriod Número de velas utilizadas para calcular el máximo más alto y el mínimo más bajo. 200
RiskPercent Porcentaje del capital de la cartera arriesgado por operación. 1%

Todos los parámetros admiten la optimización a través del optimizador incorporado.

Notas adicionales

  • La estrategia solo opera una posición a la vez, replicando la guardia PositionsTotal()>0 de la versión MQL.
  • Los precios de stop-loss y take-profit se mantienen internamente en lugar de enviar órdenes separadas, lo que mantiene la lógica cercana al Asesor Experto original sin dejar de ser compatible con el nivel alto API.
  • Cuando falta información sobre el capital de la cartera o el volumen del instrumento, la estrategia aún se negocia utilizando el volumen más pequeño posible para mantener el comportamiento determinista.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the MetaTrader strategy MeanReversion.mq5.
/// Buys when price sets a fresh lookback low and targets the mid-point of the recent range,
/// or sells at a new high aiming for the same reversion level.
/// Position size is determined from the percentage risk and the stop distance.
/// </summary>
public class MeanReversionDonchianStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _lookbackPeriod;
	private readonly StrategyParam<decimal> _riskPercent;

	private DonchianChannels _donchian = null!;

	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private Sides? _activeSide;

	/// <summary>
	/// Candle type and timeframe used for the Donchian channel calculation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Amount of candles included in the high/low range.
	/// </summary>
	public int LookbackPeriod
	{
		get => _lookbackPeriod.Value;
		set => _lookbackPeriod.Value = value;
	}

	/// <summary>
	/// Percent of portfolio equity risked per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MeanReversionDonchianStrategy"/>.
	/// </summary>
	public MeanReversionDonchianStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles to analyze", "General");

		_lookbackPeriod = Param(nameof(LookbackPeriod), 200)
		.SetDisplay("Lookback", "Number of candles used for range detection", "Signals")
		.SetRange(20, 500)
		;

		_riskPercent = Param(nameof(RiskPercent), 1m)
		.SetDisplay("Risk %", "Percentage of equity risked per entry", "Money Management")
		.SetRange(0.25m, 5m)
		;
	}

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

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

		_stopPrice = null;
		_takeProfitPrice = null;
		_activeSide = null;
	}

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

		_donchian = new DonchianChannels { Length = LookbackPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.BindEx(_donchian, ProcessCandle)
		.Start();

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

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

		// indicators bound via BindEx

		ManageOpenPosition(candle);

		if (Position != 0)
		return;

		if (donchianValue is not IDonchianChannelsValue channel)
			return;

		if (channel.UpperBand is not decimal upperBand || channel.LowerBand is not decimal lowerBand || channel.Middle is not decimal midBand)
		return;

		GenerateSignals(candle, lowerBand, upperBand, midBand);
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0 && _activeSide == Sides.Buy)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Position);
				ResetPositionState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Position);
				ResetPositionState();
			}
		}
		else if (Position < 0 && _activeSide == Sides.Sell)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(-Position);
				ResetPositionState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(-Position);
				ResetPositionState();
			}
		}

		if (Position == 0 && _activeSide != null)
		{
			ResetPositionState();
		}
	}

	private void GenerateSignals(ICandleMessage candle, decimal lowerBand, decimal upperBand, decimal midBand)
	{
		var closePrice = candle.ClosePrice;

		if (candle.LowPrice <= lowerBand)
		{
			var stopPrice = 2m * closePrice - midBand;
			var volume = CalculateRiskAdjustedVolume(closePrice, stopPrice);
			if (volume > 0m && stopPrice < closePrice)
			{
				BuyMarket(volume);
				_stopPrice = stopPrice;
				_takeProfitPrice = midBand;
				_activeSide = Sides.Buy;
			}
		}
		else if (candle.HighPrice >= upperBand)
		{
			var stopPrice = 2m * closePrice - midBand;
			var volume = CalculateRiskAdjustedVolume(closePrice, stopPrice);
			if (volume > 0m && stopPrice > closePrice)
			{
				SellMarket(volume);
				_stopPrice = stopPrice;
				_takeProfitPrice = midBand;
				_activeSide = Sides.Sell;
			}
		}
	}

	private decimal CalculateRiskAdjustedVolume(decimal entryPrice, decimal stopPrice)
	{
		var perUnitRisk = Math.Abs(entryPrice - stopPrice);
		if (perUnitRisk <= 0m)
		return 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;
		var riskBudget = portfolioValue > 0m ? portfolioValue * (RiskPercent / 100m) : 0m;

		if (riskBudget <= 0m)
		{
			return GetMinimalVolume();
		}

		var rawVolume = riskBudget / perUnitRisk;
		var normalized = NormalizeVolume(rawVolume);
		var minimal = GetMinimalVolume();

		if (normalized < minimal)
		normalized = minimal;

		return normalized;
	}

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

		var step = Security?.VolumeStep ?? 0m;
		if (step <= 0m)
		return volume;

		var normalized = Math.Floor(volume / step) * step;

		var max = Security?.MaxVolume ?? 0m;
		if (max > 0m && normalized > max)
		normalized = max;

		return normalized;
	}

	private decimal GetMinimalVolume()
	{
		var min = Security?.MinVolume ?? 0m;
		if (min > 0m)
		return min;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		return step;

		return Volume > 0m ? Volume : 1m;
	}

	private void ResetPositionState()
	{
		_stopPrice = null;
		_takeProfitPrice = null;
		_activeSide = null;
	}
}