Die Donchain-Zähler-Strategie ist eine StockSharp-Portierung des MQL5-Expertenberaters „Donchain counter" von Michal Rutka. Das System beobachtet, wie sich der Donchian-Kanal ausdehnt, um Ausbrüche zu erkennen, und verteidigt dann die Position, indem es den Stop entlang der gegenüberliegenden Band nachzieht, sobald der Preis eine feste Distanz zurückgelegt hat. Es kann nur eine Position alle 24 Stunden eröffnet werden, was die ursprüngliche Einschränkung widerspiegelt.
Handelslogik
Long-Einstiege
Signale werden auf abgeschlossenen Kerzen des konfigurierten Zeitrahmens ausgewertet (Standard H1).
Die obere Donchian-Band der letzten zwei geschlossenen Bars wird beobachtet. Wenn die Band bei Bar t-1 höher ist als bei t-2 (ein frischer Ausbruch über das Kanalhoch), wird eine Long-Market-Order platziert.
Der anfängliche Schutz-Stop wird an der aktuellen unteren Donchian-Band verankert.
Short-Einstiege
Die untere Donchian-Band der letzten zwei geschlossenen Bars wird überwacht. Wenn die Band bei Bar t-1 niedriger ist als bei t-2 (ein Ausbruch unter das Kanaltief), wird eine Short-Market-Order übermittelt.
Das erste Stop-Niveau wird auf die aktuelle obere Donchian-Band gesetzt.
Handels-Abkühlzeit
Nach jedem neuen Einstieg zeichnet der Algorithmus die Ausführungszeit auf und blockiert weitere Einstiege für die Dauer von TradeCooldown (Standard 24 Stunden). Dies reproduziert die Regel „nur ein Trade pro Tag" der MQL-Version.
Trailing- und Ausstiegsregeln
Ein Trailing-Mechanismus greift erst, wenn der Preis mindestens BufferSteps Preisschritte über die gegenüberliegende Donchian-Band hinaus vorrückt. Dies repliziert die Anforderung des ursprünglichen EA, bei der sich der Markt 50 Punkte bewegen muss, bevor der Stop enger gesetzt wird.
Long-Positionen: Sobald der Trailing-Trigger ausgelöst wird, wird der Stop auf die aktuelle untere Band aktualisiert. Wenn das Tief der Kerze dieses Niveau berührt, steigt die Strategie mit einer Market-Order aus.
Short-Positionen: Nach dem Trigger folgt der Stop der aktuellen oberen Band. Wenn das Hoch der Kerze diesen Preis erreicht, wird die Position geschlossen.
Wenn ein Trailing-Stop einen Ausstieg erzwingt, öffnet die Strategie erst eine neue Position, wenn das nächste Signal und die Abkühlzeit es erlauben.
Risikoverwaltung
Die Strategie handelt immer eine einzelne Position, deren Größe durch den Parameter Volume definiert ist.
Es gibt kein Gewinnziel; alle Ausstiege werden durch die Donchian-Trailing-Logik gesteuert.
Parameter
Name
Beschreibung
Standard
Volume
Ordergröße für Einstiege.
1
ChannelPeriod
Rückblickperiode für die Donchian-Kanal-Berechnung.
20
BufferSteps
Anzahl der Preisschritte, um die der Preis über die gegenüberliegende Band hinausgehen muss, bevor das Trailing aktiviert wird (MQL verwendete 50 Punkte).
50
TradeCooldown
Mindestzeit zwischen neuen Einstiegen.
1 Tag
CandleType
Kerzenserie für den Indikator (Standard 1-Stunden-Kerzen).
1h-Kerzen
Indikatoren
Donchian-Kanäle – die obere und untere Band definieren Ausbruchssignale und dynamische Stops.
Hinweise
Verwenden Sie Instrumente mit einem sinnvollen PriceStep, damit der Buffer in eine realistische Preisdistanz übersetzt wird. Die Strategie verwendet standardmäßig einen Schritt von 0.0001, wenn keiner vom Instrument angegeben wird.
Es ist immer nur eine Richtung offen. Vor einem Richtungswechsel muss die bestehende Position vollständig geschlossen sein, genau wie beim ursprünglichen Expertenberater.
Chart-Objekte werden automatisch vorbereitet, wenn ein Chart-Bereich verfügbar ist: Kerzen, der Donchian-Kanal und die eigenen Trades der Strategie.
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>
/// Donchain Counter strategy converted from MQL5 Donchain counter expert advisor.
/// Tracks Donchian channel expansions for entries and trails stops along the channel bands.
/// </summary>
public class DonchainCounterStrategy : Strategy
{
private readonly StrategyParam<int> _channelPeriod;
private readonly StrategyParam<int> _bufferSteps;
private readonly StrategyParam<TimeSpan> _tradeCooldown;
private readonly StrategyParam<DataType> _candleType;
private DonchianChannels _donchian = null!;
private decimal _priceStep;
private decimal _tolerance;
private decimal _currentUpper;
private decimal _currentLower;
private decimal _previousUpper;
private decimal _previousLower;
private decimal _earlierUpper;
private decimal _earlierLower;
private decimal? _longStopLevel;
private decimal? _shortStopLevel;
private DateTimeOffset? _lastTradeTime;
/// <summary>
/// Initializes a new instance of <see cref="DonchainCounterStrategy"/>.
/// </summary>
public DonchainCounterStrategy()
{
_channelPeriod = Param(nameof(ChannelPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Donchian Period", "Lookback period for Donchian Channel", "Indicators")
.SetOptimize(10, 40, 5);
_bufferSteps = Param(nameof(BufferSteps), 50)
.SetGreaterThanZero()
.SetDisplay("Buffer Steps", "Minimum price steps before trailing stop activates", "Risk");
_tradeCooldown = Param(nameof(TradeCooldown), TimeSpan.FromMinutes(30))
.SetDisplay("Trade Cooldown", "Minimum waiting time between new entries", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle series used for Donchian evaluation", "General");
}
/// <summary>
/// Donchian channel lookback period.
/// </summary>
public int ChannelPeriod
{
get => _channelPeriod.Value;
set => _channelPeriod.Value = value;
}
/// <summary>
/// Number of price steps that price must move beyond the opposite band before trailing starts.
/// </summary>
public int BufferSteps
{
get => _bufferSteps.Value;
set => _bufferSteps.Value = value;
}
/// <summary>
/// Minimum cooldown between new trades.
/// </summary>
public TimeSpan TradeCooldown
{
get => _tradeCooldown.Value;
set => _tradeCooldown.Value = value;
}
/// <summary>
/// Candle type for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_donchian = null!;
_priceStep = 0m;
_tolerance = 0m;
_currentUpper = 0m;
_currentLower = 0m;
_previousUpper = 0m;
_previousLower = 0m;
_earlierUpper = 0m;
_earlierLower = 0m;
_longStopLevel = null;
_shortStopLevel = null;
_lastTradeTime = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_priceStep = Security?.PriceStep ?? 0m;
if (_priceStep <= 0m)
{
_priceStep = 0.0001m;
}
_tolerance = _priceStep / 2m;
_donchian = new DonchianChannels
{
Length = ChannelPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessDonchianRaw)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessDonchianRaw(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var donchianValue = _donchian.Process(candle);
if (donchianValue.IsEmpty || !_donchian.IsFormed)
return;
var value = (DonchianChannelsValue)donchianValue;
if (value.UpperBand is not decimal upperBand || value.LowerBand is not decimal lowerBand)
return;
_currentUpper = upperBand;
_currentLower = lowerBand;
var hadPosition = Position != 0m;
if (hadPosition)
{
ManageExistingPosition(candle);
UpdateHistory();
return;
}
TryOpenPosition(candle);
UpdateHistory();
}
private void ManageExistingPosition(ICandleMessage candle)
{
// Buffer converts the point-based activation threshold into price units.
var buffer = BufferSteps * _priceStep;
if (Position > 0m)
{
// Activate or advance the trailing stop once price moves far enough from the lower band.
if (candle.HighPrice > _currentLower + buffer)
{
if (!_longStopLevel.HasValue || _longStopLevel.Value < _currentLower - _tolerance)
{
_longStopLevel = _currentLower;
// Log: $"Updated long stop to {_longStopLevel.Value}");
}
}
// Exit the long position when price falls back through the protected band.
if (_longStopLevel.HasValue && candle.LowPrice <= _longStopLevel.Value + _tolerance)
{
SellMarket(Position);
// Log: $"Long exit triggered at {_longStopLevel.Value}");
_longStopLevel = null;
}
}
else if (Position < 0m)
{
// Activate or advance the trailing stop once price moves far enough from the upper band.
if (candle.LowPrice < _currentUpper - buffer)
{
if (!_shortStopLevel.HasValue || _shortStopLevel.Value > _currentUpper + _tolerance)
{
_shortStopLevel = _currentUpper;
// Log: $"Updated short stop to {_shortStopLevel.Value}");
}
}
// Exit the short position when price rallies back to the protected band.
if (_shortStopLevel.HasValue && candle.HighPrice >= _shortStopLevel.Value - _tolerance)
{
BuyMarket(Math.Abs(Position));
// Log: $"Short exit triggered at {_shortStopLevel.Value}");
_shortStopLevel = null;
}
}
else
{
_longStopLevel = null;
_shortStopLevel = null;
}
}
private void TryOpenPosition(ICandleMessage candle)
{
// Require at least two completed Donchian samples for breakout comparisons.
if (_previousUpper == 0m || _earlierUpper == 0m || _previousLower == 0m || _earlierLower == 0m)
{
return;
}
var now = candle.CloseTime;
if (_lastTradeTime.HasValue && now - _lastTradeTime.Value < TradeCooldown)
{
return;
}
// Long entry when the upper Donchian band expanded on the previous bar.
if (_previousUpper > _earlierUpper && !AreClose(_previousUpper, _earlierUpper))
{
BuyMarket(Volume);
_longStopLevel = _currentLower;
_lastTradeTime = now;
// Log: $"Long entry at {candle.ClosePrice} with stop {_currentLower}");
return;
}
// Short entry when the lower Donchian band contracted on the previous bar.
if (_previousLower < _earlierLower && !AreClose(_previousLower, _earlierLower))
{
SellMarket(Volume);
_shortStopLevel = _currentUpper;
_lastTradeTime = now;
// Log: $"Short entry at {candle.ClosePrice} with stop {_currentUpper}");
}
}
private void UpdateHistory()
{
_earlierUpper = _previousUpper;
_earlierLower = _previousLower;
_previousUpper = _currentUpper;
_previousLower = _currentLower;
}
private bool AreClose(decimal first, decimal second)
{
return Math.Abs(first - second) <= _tolerance;
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (Position == 0m)
{
_longStopLevel = null;
_shortStopLevel = null;
}
else if (Position > 0m)
{
_shortStopLevel = null;
}
else
{
_longStopLevel = null;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import DonchianChannels, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class donchain_counter_strategy(Strategy):
def __init__(self):
super(donchain_counter_strategy, self).__init__()
self._channel_period = self.Param("ChannelPeriod", 20)
self._buffer_steps = self.Param("BufferSteps", 50)
self._trade_cooldown = self.Param("TradeCooldown", TimeSpan.FromMinutes(30))
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._donchian = None
self._price_step = 0.0
self._tolerance = 0.0
self._current_upper = 0.0
self._current_lower = 0.0
self._previous_upper = 0.0
self._previous_lower = 0.0
self._earlier_upper = 0.0
self._earlier_lower = 0.0
self._long_stop = None
self._short_stop = None
self._last_trade_time = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(donchain_counter_strategy, self).OnStarted2(time)
self._price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0001
if self._price_step <= 0:
self._price_step = 0.0001
self._tolerance = self._price_step / 2.0
self._donchian = DonchianChannels()
self._donchian.Length = self._channel_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
civ = CandleIndicatorValue(self._donchian, candle)
civ.IsFinal = True
result = self._donchian.Process(civ)
if result.IsEmpty or not self._donchian.IsFormed:
return
ub = result.UpperBand
lb = result.LowerBand
if ub is None or lb is None:
return
self._current_upper = float(ub)
self._current_lower = float(lb)
if self.Position != 0:
self._manage_position(candle)
self._update_history()
return
self._try_open(candle)
self._update_history()
def _manage_position(self, candle):
buffer = self._buffer_steps.Value * self._price_step
if self.Position > 0:
if float(candle.HighPrice) > self._current_lower + buffer:
if self._long_stop is None or self._long_stop < self._current_lower - self._tolerance:
self._long_stop = self._current_lower
if self._long_stop is not None and float(candle.LowPrice) <= self._long_stop + self._tolerance:
self.SellMarket(self.Position)
self._long_stop = None
elif self.Position < 0:
if float(candle.LowPrice) < self._current_upper - buffer:
if self._short_stop is None or self._short_stop > self._current_upper + self._tolerance:
self._short_stop = self._current_upper
if self._short_stop is not None and float(candle.HighPrice) >= self._short_stop - self._tolerance:
self.BuyMarket(abs(self.Position))
self._short_stop = None
def _try_open(self, candle):
if self._previous_upper == 0 or self._earlier_upper == 0 or self._previous_lower == 0 or self._earlier_lower == 0:
return
now = candle.CloseTime
if self._last_trade_time is not None and (now - self._last_trade_time) < self._trade_cooldown.Value:
return
if self._previous_upper > self._earlier_upper and not self._are_close(self._previous_upper, self._earlier_upper):
self.BuyMarket(self.Volume)
self._long_stop = self._current_lower
self._last_trade_time = now
return
if self._previous_lower < self._earlier_lower and not self._are_close(self._previous_lower, self._earlier_lower):
self.SellMarket(self.Volume)
self._short_stop = self._current_upper
self._last_trade_time = now
def _update_history(self):
self._earlier_upper = self._previous_upper
self._earlier_lower = self._previous_lower
self._previous_upper = self._current_upper
self._previous_lower = self._current_lower
def _are_close(self, first, second):
return abs(first - second) <= self._tolerance
def OnReseted(self):
super(donchain_counter_strategy, self).OnReseted()
self._current_upper = 0.0
self._current_lower = 0.0
self._previous_upper = 0.0
self._previous_lower = 0.0
self._earlier_upper = 0.0
self._earlier_lower = 0.0
self._long_stop = None
self._short_stop = None
self._last_trade_time = None
def CreateClone(self):
return donchain_counter_strategy()