Esta estrategia es una adaptación del MetaTrader asesor experto MeanReversion.mq5. Opera con un patrón simple de reversión a la media: cada vez que el precio registra un nuevo mínimo dentro de la ventana retrospectiva seleccionada, la estrategia abre una posición larga, apuntando al punto medio del rango reciente. Cuando aparece un nuevo máximo, la estrategia refleja la lógica del lado corto. El tamaño de la posición se calcula a partir del porcentaje de riesgo y la distancia de parada, replicando fielmente el cálculo del lote que realiza el EA original.
Lógica de trading
Cree un canal Donchian utilizando el tipo de vela configurado y el período retrospectivo. La banda superior marca el máximo más alto y la banda inferior el mínimo más bajo sobre la ventana. El punto medio (upper + lower) / 2 actúa como objetivo de reversión a la media.
Si la vela finalizada actual alcanza un nuevo mínimo (Low <= LowerBand) y no hay ninguna posición abierta, la estrategia compra en el mercado. La parada de protección se refleja alrededor del precio de entrada, de modo que el punto medio se convierte en el objetivo de ganancias, coincidiendo con el cálculo MetaTrader sl = 2 * Ask - tp.
Si la vela alcanza un nuevo máximo (High >= UpperBand) y no hay ninguna posición abierta, la estrategia vende en el mercado con un stop simétrico por encima del precio. El punto medio vuelve a actuar como nivel de obtención de beneficios.
El stop-loss y el take-profit se controlan en cada vela terminada. Una ruptura más allá del stop cierra la posición inmediatamente, mientras que al tocar el punto medio se sale de la operación en el objetivo previsto. El estado interno se reinicia automáticamente cada vez que la posición es plana.
Dimensionamiento de posiciones
El riesgo por operación es igual a Portfolio.CurrentValue * (RiskPercent / 100). Si los datos de la cartera no están disponibles, la estrategia vuelve al volumen mínimo negociable.
El riesgo del contrato se mide como |EntryPrice - StopPrice|. El volumen bruto es RiskAmount / perUnitRisk y está normalizado al paso de volumen del instrumento. Se respetan las restricciones cambiarias mínimas y máximas. Cuando el volumen normalizado es menor que el tamaño mínimo comercializable, se utiliza el mínimo.
Parámetros
Nombre
Descripción
Predeterminado
CandleType
Tipo de vela y período de tiempo utilizados para crear el canal Donchian.
plazo de 15 minutos
LookbackPeriod
Número de velas utilizadas para calcular el máximo más alto y el mínimo más bajo.
200
RiskPercent
Porcentaje del capital de la cartera arriesgado por operación.
1%
Todos los parámetros admiten la optimización a través del optimizador incorporado.
Notas adicionales
La estrategia solo opera una posición a la vez, replicando la guardia PositionsTotal()>0 de la versión MQL.
Los precios de stop-loss y take-profit se mantienen internamente en lugar de enviar órdenes separadas, lo que mantiene la lógica cercana al Asesor Experto original sin dejar de ser compatible con el nivel alto API.
Cuando falta información sobre el capital de la cartera o el volumen del instrumento, la estrategia aún se negocia utilizando el volumen más pequeño posible para mantener el comportamiento determinista.
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()