Стратегия Mean Reversion Donchian
Обзор
Стратегия является портом MetaTrader-советника MeanReversion.mq5. При появлении нового минимума в пределах заданного окна она открывает длинную позицию и нацеливается на середину диапазона; при обновлении максимума выполняет зеркальное короткое вход. Размер позиции рассчитывается по проценту риска и расстоянию до стоп-уровня, что максимально приближено к алгоритму подбора лота в оригинальном советнике.
Логика торговли
- По выбранному типу свечей и периоду строится канал Дончиана. Верхняя граница отражает максимумы, нижняя — минимумы, а средняя линия
(upper + lower) / 2 используется как цель возврата к среднему.
- Если завершившаяся свеча формирует новый минимум (
Low <= LowerBand) и позиций нет, выполняется покупка по рынку. Стоп ставится симметрично относительно цены входа так, чтобы целевой уровень совпадал со средней линией. Это повторяет формулу из MQL sl = 2 * Ask - tp.
- Если свеча формирует новый максимум (
High >= UpperBand) при отсутствии позиции, выполняется продажа по рынку со стоп-уровнем над ценой входа, а цель снова равна середине канала.
- На каждой завершённой свече проверяются условия выхода. Пробой стопа закрывает позицию сразу, достижение средней линии фиксирует прибыль. После возврата к нулевой позиции внутреннее состояние автоматически сбрасывается.
Управление объёмом
- Риск на сделку рассчитывается как
Portfolio.CurrentValue * (RiskPercent / 100). При отсутствии данных по счёту используется минимально допустимый объём.
- Риск на единицу инструмента равен
|EntryPrice - StopPrice|. Исходный объём — это RiskAmount / perUnitRisk, после чего он нормализуется под шаг объёма инструмента с учётом ограничений по минимуму и максимуму. Если нормализованное значение меньше минимально допустимого, используется именно минимальный объём.
Параметры
| Имя |
Описание |
Значение по умолчанию |
CandleType |
Тип и таймфрейм свечей для построения канала Дончиана. |
15 минут |
LookbackPeriod |
Количество свечей в окне поиска максимумов и минимумов. |
200 |
RiskPercent |
Процент капитала, которым рискнут в одной сделке. |
1% |
Все параметры доступны для оптимизации.
Дополнительные замечания
- В каждый момент времени стратегия держит только одну позицию, что соответствует проверке
PositionsTotal()>0 в исходнике MQL.
- Стоп и тейк-профит поддерживаются внутренней логикой без выставления дополнительных заявок, что делает поведение ближе к оригиналу и совместимым с High Level API.
- Если отсутствуют данные о капитале или шагах объёма, стратегия всё равно торгует минимально возможным объёмом, обеспечивая предсказуемость работы.
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()