Ver no GitHub

Estratégia de reversão à média Donchian

Visão geral

Esta estratégia é uma versão do MetaTrader consultor especialista MeanReversion.mq5. Ela negocia um padrão simples de reversão à média: sempre que o preço imprime um novo mínimo dentro da janela de lookback selecionada, a estratégia abre uma posição longa, visando o ponto médio da faixa recente. Quando uma nova máxima aparece, a estratégia reflete a lógica do lado vendido. O tamanho da posição é calculado a partir da porcentagem de risco e da distância de parada, replicando de perto o cálculo do lote executado pelo EA original.

Lógica de negociação

  1. Crie um canal Donchian usando o tipo de vela configurado e o período de lookback. A faixa superior marca o máximo mais alto e a faixa inferior marca o mínimo mais baixo sobre a janela. O ponto médio (upper + lower) / 2 atua como o alvo de reversão à média.
  2. Se a vela finalizada atual atingir um novo mínimo (Low <= LowerBand) e nenhuma posição estiver aberta, a estratégia compra no mercado. O stop protetor é refletido em torno do preço de entrada para que o ponto médio se torne a meta de lucro, correspondendo ao cálculo MetaTrader sl = 2 * Ask - tp.
  3. Se a vela atingir uma nova máxima (High >= UpperBand) e nenhuma posição estiver aberta, a estratégia vende a mercado com um stop simétrico acima do preço. O ponto médio novamente atua como o nível de lucro.
  4. O stop-loss e o take-profit são monitorados em cada vela finalizada. Um rompimento além do stop fecha a posição imediatamente, enquanto tocar o ponto médio sai da negociação no alvo pretendido. O estado interno é redefinido automaticamente sempre que a posição é plana.

Dimensionamento de posições

  • O risco por negociação é igual a Portfolio.CurrentValue * (RiskPercent / 100). Se os dados da carteira não estiverem disponíveis, a estratégia volta ao volume negociável mínimo.
  • O risco do contrato é medido como |EntryPrice - StopPrice|. O volume bruto é RiskAmount / perUnitRisk e é normalizado para a etapa de volume do instrumento. As restrições cambiais mínimas e máximas são respeitadas. Quando o volume normalizado é menor que o tamanho mínimo negociável, o mínimo é usado.

Parâmetros

Nome Descrição Padrão
CandleType Tipo de vela e período usado para construir o canal Donchian. Período de 15 minutos
LookbackPeriod Número de velas usadas para calcular a máxima mais alta e a mínima mais baixa. 200
RiskPercent Porcentagem do patrimônio do portfólio arriscado por negociação. 1%

Todos os parâmetros suportam otimização por meio do otimizador integrado.

Notas adicionais

  • A estratégia negocia apenas uma posição por vez, replicando a guarda PositionsTotal()>0 da versão MQL.
  • Os preços stop-loss e take-profit são mantidos internamente em vez de enviar pedidos separados, o que mantém a lógica próxima do Expert Advisor original, permanecendo compatível com o API de alto nível.
  • Quando faltam informações sobre o patrimônio do portfólio ou sobre o volume do instrumento, a estratégia ainda negocia usando o menor volume possível para manter o comportamento determinístico.
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;
	}
}