Auf GitHub ansehen

Mean-Reversion-Donchian-Strategie

Überblick

Diese Strategie ist eine Portierung des MetaTrader-Expertenberaters MeanReversion.mq5. Dabei handelt es sich um ein einfaches Mean-Reversion-Muster: Immer wenn der Preis innerhalb des ausgewählten Lookback-Fensters ein neues Tief erreicht, eröffnet die Strategie eine Long-Position und zielt auf die Mitte der aktuellen Spanne. Wenn ein neues Hoch erscheint, spiegelt die Strategie die Logik auf der Short-Seite wider. Die Positionsgröße wird aus dem Risikoprozentsatz und der Stoppdistanz berechnet und ist eine weitgehende Nachbildung der Lotberechnung, die das ursprüngliche EA durchführt.

Handelslogik

  1. Erstellen Sie einen Donchian-Kanal mit dem konfigurierten Kerzentyp und Lookback-Zeitraum. Das obere Band markiert das höchste Hoch und das untere Band das niedrigste Tief über dem Fenster. Der Mittelpunkt (upper + lower) / 2 fungiert als mittleres Umkehrziel.
  2. Wenn die aktuelle fertige Kerze ein neues Tief (Low <= LowerBand) erreicht und keine Position offen ist, kauft die Strategie zum Markt. Der Schutzstopp spiegelt sich um den Einstiegspreis wider, sodass der Mittelpunkt zum Gewinnziel wird und der MetaTrader-Berechnung sl = 2 * Ask - tp entspricht.
  3. Wenn die Kerze ein neues Hoch (High >= UpperBand) erreicht und keine Position offen ist, wird die Strategie zum Marktpreis mit einem symmetrischen Stop über dem Preis verkauft. Der Mittelpunkt fungiert wiederum als Take-Profit-Niveau.
  4. Stop-Loss und Take-Profit werden bei jeder fertigen Kerze überwacht. Bei einem Ausbruch über den Stop hinaus wird die Position sofort geschlossen, während bei Erreichen des Mittelpunkts der Handel am beabsichtigten Ziel beendet wird. Der interne Zustand wird automatisch zurückgesetzt, wenn die Position flach ist.

Positionsgrößen

  • Das Risiko pro Trade beträgt Portfolio.CurrentValue * (RiskPercent / 100). Liegen keine Portfoliodaten vor, greift die Strategie auf das minimal handelbare Volumen zurück.
  • Das Vertragsrisiko wird mit |EntryPrice - StopPrice| gemessen. Das Rohvolumen beträgt RiskAmount / perUnitRisk und wird auf den Lautstärkeschritt des Instruments normalisiert. Die minimalen und maximalen Wechselkursbeschränkungen werden eingehalten. Wenn das normalisierte Volumen kleiner als die minimale handelbare Größe ist, wird stattdessen das Minimum verwendet.

Parameter

Name Beschreibung Standard
CandleType Kerzentyp und Zeitrahmen, die für den Aufbau des Donchian-Kanals verwendet werden. 15-minütiger Zeitrahmen
LookbackPeriod Anzahl der Kerzen, die zur Berechnung des höchsten Hochs und niedrigsten Tiefs verwendet werden. 200
RiskPercent Prozentsatz des pro Trade riskierten Portfolio-Eigenkapitals. 1 %

Alle Parameter unterstützen die Optimierung durch den integrierten Optimierer.

Zusätzliche Hinweise

  • Die Strategie handelt jeweils nur eine Position und repliziert den PositionsTotal()>0-Guard aus der MQL-Version.
  • Stop-Loss- und Take-Profit-Preise werden intern verwaltet, anstatt separate Aufträge zu senden, wodurch die Logik nah am ursprünglichen Expert Advisor bleibt und gleichzeitig mit dem High-Level-API kompatibel bleibt.
  • Wenn Informationen zum Portfolio-Aktien- oder Instrumentenvolumen fehlen, handelt die Strategie immer noch mit dem kleinstmöglichen Volumen, um das Verhalten deterministisch zu halten.
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;
	}
}