Donchain Counter — перенос эксперта MQL5 «Donchain counter» (Michal Rutka) на StockSharp. Алгоритм отслеживает расширения канала Дончиана, чтобы фиксировать пробои, и после входа переносит защитный стоп вдоль противоположной границы канала, когда цена уходит достаточно далеко. Как и в оригинале, открыть новую позицию разрешается не чаще одного раза в сутки.
Логика торговли
Вход в лонг
Сигналы анализируются только по завершённым свечам выбранного таймфрейма (по умолчанию H1).
Если верхняя граница канала Дончиана на свече t-1 выше, чем на свече t-2, значит произошёл пробой максимума — открывается рыночная покупка.
Первоначальный стоп помещается на текущую нижнюю границу канала.
Вход в шорт
Если нижняя граница канала Дончиана на свече t-1 ниже, чем на свече t-2, фиксируется пробой минимума — отправляется рыночная продажа.
Первоначальный стоп выставляется на текущую верхнюю границу канала.
Ограничение частоты сделок
После каждой новой позиции фиксируется время входа. Пока не пройдёт TradeCooldown (по умолчанию 24 часа), новые сделки не открываются. Это повторяет правило «не более одной сделки в день» из MQL-версии.
Сопровождение и выход
Трейлинг активируется только после того, как цена уйдёт минимум на BufferSteps шагов цены от противоположной границы канала. В оригинале требовалось 50 пунктов.
Лонг: после активации стоп переносится на текущую нижнюю границу. Если минимум свечи касается этого уровня, позиция закрывается рыночной продажей.
Шорт: после активации стоп следует за текущей верхней границей. Если максимум свечи достигает уровня, позиция закрывается покупкой.
После закрытия по стопу стратегия ждёт следующего сигнала и окончания периода ожидания, прежде чем открывать новую позицию.
Управление рисками
Торговый объём задаётся параметром Volume; одновременно открыта только одна позиция.
Цели по прибыли не используются — выход осуществляется исключительно по правилам канала Дончиана.
Параметры
Имя
Описание
Значение по умолчанию
Volume
Объём заявки.
1
ChannelPeriod
Период расчёта канала Дончиана.
20
BufferSteps
Количество шагов цены, на которое цена должна уйти за пределы противоположной границы до активации трейлинга.
50
TradeCooldown
Минимальный интервал между входами.
1 день
CandleType
Тип свечей для анализа (по умолчанию часовые).
1h
Индикаторы
Donchian Channels — верхняя и нижняя границы задают сигналы на вход и уровни защитных стопов.
Особенности
Желательно использовать инструменты с корректно заданным PriceStep, чтобы буфер BufferSteps соответствовал реальному расстоянию. При отсутствии шага по инструменту стратегия использует значение 0.0001.
Одновременные разнонаправленные позиции не допускаются — перед сменой направления текущая позиция полностью закрывается.
Если доступна область графика, стратегия автоматически добавляет свечи, канал Дончиана и собственные сделки.
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()