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
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.
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.
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.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Indicators import DonchianChannels
from StockSharp.Algo.Strategies import Strategy
class mean_reversion_donchian_strategy(Strategy):
"""Buys at Donchian low, sells at Donchian high, targeting the midpoint."""
def __init__(self):
super(mean_reversion_donchian_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Type of candles to analyze", "General")
self._lookback_period = self.Param("LookbackPeriod", 200) \
.SetDisplay("Lookback", "Number of candles used for range detection", "Signals")
self._risk_percent = self.Param("RiskPercent", 1.0) \
.SetDisplay("Risk %", "Percentage of equity risked per entry", "Money Management")
self._stop_price = None
self._take_profit_price = None
self._active_side = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def LookbackPeriod(self):
return self._lookback_period.Value
@property
def RiskPercent(self):
return self._risk_percent.Value
def OnReseted(self):
super(mean_reversion_donchian_strategy, self).OnReseted()
self._stop_price = None
self._take_profit_price = None
self._active_side = None
def OnStarted2(self, time):
super(mean_reversion_donchian_strategy, self).OnStarted2(time)
donchian = DonchianChannels()
donchian.Length = self.LookbackPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(donchian, self._process_candle).Start()
def _process_candle(self, candle, donchian_value):
if candle.State != CandleStates.Finished:
return
self._manage_open_position(candle)
if self.Position != 0:
return
upper = donchian_value.UpperBand
lower = donchian_value.LowerBand
middle = donchian_value.Middle
if upper is None or lower is None or middle is None:
return
up = float(upper)
lo = float(lower)
mid = float(middle)
close = float(candle.ClosePrice)
if float(candle.LowPrice) <= lo:
stop_p = 2.0 * close - mid
if stop_p < close:
self.BuyMarket()
self._stop_price = stop_p
self._take_profit_price = mid
self._active_side = Sides.Buy
elif float(candle.HighPrice) >= up:
stop_p = 2.0 * close - mid
if stop_p > close:
self.SellMarket()
self._stop_price = stop_p
self._take_profit_price = mid
self._active_side = Sides.Sell
def _manage_open_position(self, candle):
if self.Position > 0 and self._active_side == Sides.Buy:
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(self.Position)
self._reset_state()
return
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket(self.Position)
self._reset_state()
elif self.Position < 0 and self._active_side == Sides.Sell:
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(self.Position))
self._reset_state()
return
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket(abs(self.Position))
self._reset_state()
if self.Position == 0 and self._active_side is not None:
self._reset_state()
def _reset_state(self):
self._stop_price = None
self._take_profit_price = None
self._active_side = None
def CreateClone(self):
return mean_reversion_donchian_strategy()