Estrategia ExpXmaRangeBands
Esta estrategia replica la lógica del ejemplo de MetaTrader "Exp_XMA_Range_Bands" utilizando la API de alto nivel de StockSharp. Emplea un Canal de Keltner para definir soporte y resistencia dinámicos basados en una media móvil y el rango verdadero promedio. Las operaciones se activan cuando el precio vuelve a entrar al canal después de haber salido.
Cómo funciona
- Construir un Canal de Keltner utilizando:
- Periodo de EMA
MaLength - Periodo de ATR
RangeLength - Multiplicador del ATR
Deviation
- Periodo de EMA
- Cuando una vela cierra por encima de la banda superior anterior, se cierra cualquier posición corta. Si la siguiente vela cierra de nuevo dentro del canal (cierre ≤ banda superior actual), se abre una posición larga.
- Cuando una vela cierra por debajo de la banda inferior anterior, se cierra cualquier posición larga. Si la siguiente vela cierra de nuevo dentro del canal (cierre ≥ banda inferior actual), se abre una posición corta.
- Los niveles de stop-loss y take-profit se expresan en puntos y se aplican una vez que se entra en una posición.
Parámetros
MaLength– Periodo de EMA para el centro del canal.RangeLength– Periodo de ATR utilizado para el ancho del canal.Deviation– Multiplicador aplicado al ATR para calcular las bandas.StopLoss– Stop-loss en puntos (convertido a precio medianteSecurity.PriceStep).TakeProfit– Take-profit en puntos (convertido a precio medianteSecurity.PriceStep).CandleType– Serie de velas utilizada para los cálculos.
Indicadores
- KeltnerChannels (EMA + ATR)
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>
/// Strategy converted from the MetaTrader example "Exp_XMA_Range_Bands".
/// Uses an EMA and ATR to build dynamic bands and trades when price re-enters the channel.
/// </summary>
public class ExpXmaRangeBandsStrategy : Strategy
{
private static readonly TimeSpan _signalCooldown = TimeSpan.FromHours(8);
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _rangeLength;
private readonly StrategyParam<decimal> _deviation;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevUpper;
private decimal _prevLower;
private decimal _prevClose;
private decimal _entryPrice;
private bool _isFirst = true;
private DateTime _lastSignalTime;
/// <summary>
/// EMA period for the channel center.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// ATR period used for channel width.
/// </summary>
public int RangeLength
{
get => _rangeLength.Value;
set => _rangeLength.Value = value;
}
/// <summary>
/// ATR multiplier for band width.
/// </summary>
public decimal Deviation
{
get => _deviation.Value;
set => _deviation.Value = value;
}
/// <summary>
/// Stop loss in points.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Take profit in points.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpXmaRangeBandsStrategy"/>.
/// </summary>
public ExpXmaRangeBandsStrategy()
{
_maLength = Param(nameof(MaLength), 100)
.SetGreaterThanZero()
.SetDisplay("MA Length", "EMA period for channel center", "Indicator")
.SetOptimize(50, 200, 10);
_rangeLength = Param(nameof(RangeLength), 20)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period for channel width", "Indicator")
.SetOptimize(10, 60, 5);
_deviation = Param(nameof(Deviation), 2m)
.SetGreaterThanZero()
.SetDisplay("Deviation", "ATR multiplier for channel width", "Indicator")
.SetOptimize(1m, 5m, 0.5m);
_stopLoss = Param(nameof(StopLoss), 1000m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss", "Stop loss in points", "Risk")
.SetOptimize(500m, 2000m, 500m);
_takeProfit = Param(nameof(TakeProfit), 2000m)
.SetGreaterThanZero()
.SetDisplay("Take Profit", "Take profit in points", "Risk")
.SetOptimize(1000m, 4000m, 500m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevUpper = 0;
_prevLower = 0;
_prevClose = 0;
_entryPrice = 0;
_isFirst = true;
_lastSignalTime = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var keltner = new KeltnerChannels(
new KeltnerChannelMiddle { Length = MaLength },
new AverageTrueRange { Length = RangeLength },
new KeltnerChannelBand { Length = MaLength },
new KeltnerChannelBand { Length = MaLength })
{
Multiplier = Deviation,
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(keltner, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, keltner);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue keltnerValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (keltnerValue is not IKeltnerChannelsValue keltnerTyped ||
keltnerTyped.Upper is not decimal upper ||
keltnerTyped.Lower is not decimal lower)
return;
if (_isFirst)
{
_prevUpper = upper;
_prevLower = lower;
_prevClose = candle.ClosePrice;
_isFirst = false;
return;
}
if (_prevClose > _prevUpper)
{
if (candle.ClosePrice <= upper && Position <= 0 && CanTrade(candle))
{
var volume = Position < 0 ? Volume + Math.Abs(Position) : Volume;
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_lastSignalTime = candle.CloseTime;
}
}
else if (_prevClose < _prevLower)
{
if (candle.ClosePrice >= lower && Position >= 0 && CanTrade(candle))
{
var volume = Position > 0 ? Volume + Position : Volume;
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_lastSignalTime = candle.CloseTime;
}
}
var step = Security.PriceStep ?? 1m;
var sl = step * StopLoss;
var tp = step * TakeProfit;
if (Position > 0)
{
if (candle.ClosePrice <= _entryPrice - sl || candle.ClosePrice >= _entryPrice + tp)
{
SellMarket(Position);
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (candle.ClosePrice >= _entryPrice + sl || candle.ClosePrice <= _entryPrice - tp)
{
BuyMarket(Math.Abs(Position));
_entryPrice = 0;
}
}
_prevUpper = upper;
_prevLower = lower;
_prevClose = candle.ClosePrice;
}
private bool CanTrade(ICandleMessage candle)
=> _lastSignalTime == default || candle.CloseTime >= _lastSignalTime + _signalCooldown;
}
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, DateTime
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import KeltnerChannels, KeltnerChannelMiddle, KeltnerChannelBand, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class exp_xma_range_bands_strategy(Strategy):
def __init__(self):
super(exp_xma_range_bands_strategy, self).__init__()
self._ma_length = self.Param("MaLength", 100) \
.SetDisplay("MA Length", "EMA period for channel center", "Indicator")
self._range_length = self.Param("RangeLength", 20) \
.SetDisplay("ATR Length", "ATR period for channel width", "Indicator")
self._deviation = self.Param("Deviation", 2.0) \
.SetDisplay("Deviation", "ATR multiplier for channel width", "Indicator")
self._stop_loss = self.Param("StopLoss", 1000.0) \
.SetDisplay("Stop Loss", "Stop loss in points", "Risk")
self._take_profit = self.Param("TakeProfit", 2000.0) \
.SetDisplay("Take Profit", "Take profit in points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_upper = 0.0
self._prev_lower = 0.0
self._prev_close = 0.0
self._entry_price = 0.0
self._is_first = True
self._last_signal_time = DateTime.MinValue
self._signal_cooldown = TimeSpan.FromHours(8)
@property
def MaLength(self):
return self._ma_length.Value
@MaLength.setter
def MaLength(self, value):
self._ma_length.Value = value
@property
def RangeLength(self):
return self._range_length.Value
@RangeLength.setter
def RangeLength(self, value):
self._range_length.Value = value
@property
def Deviation(self):
return self._deviation.Value
@Deviation.setter
def Deviation(self, value):
self._deviation.Value = value
@property
def StopLoss(self):
return self._stop_loss.Value
@StopLoss.setter
def StopLoss(self, value):
self._stop_loss.Value = value
@property
def TakeProfit(self):
return self._take_profit.Value
@TakeProfit.setter
def TakeProfit(self, value):
self._take_profit.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _can_trade(self, candle):
return (self._last_signal_time == DateTime.MinValue
or candle.CloseTime >= self._last_signal_time + self._signal_cooldown)
def OnStarted2(self, time):
super(exp_xma_range_bands_strategy, self).OnStarted2(time)
mid = KeltnerChannelMiddle()
mid.Length = self.MaLength
atr = AverageTrueRange()
atr.Length = self.RangeLength
upper_band = KeltnerChannelBand()
upper_band.Length = self.MaLength
lower_band = KeltnerChannelBand()
lower_band.Length = self.MaLength
keltner = KeltnerChannels(mid, atr, upper_band, lower_band)
keltner.Multiplier = self.Deviation
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.BindEx(keltner, self.ProcessCandle) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, keltner)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, keltner_value):
if candle.State != CandleStates.Finished:
return
upper_raw = keltner_value.Upper
lower_raw = keltner_value.Lower
if upper_raw is None or lower_raw is None:
return
upper = float(upper_raw)
lower = float(lower_raw)
close = float(candle.ClosePrice)
if self._is_first:
self._prev_upper = upper
self._prev_lower = lower
self._prev_close = close
self._is_first = False
return
if self._prev_close > self._prev_upper:
if close <= upper and self.Position <= 0 and self._can_trade(candle):
volume = self.Volume + abs(self.Position) if self.Position < 0 else self.Volume
self.BuyMarket(volume)
self._entry_price = close
self._last_signal_time = candle.CloseTime
elif self._prev_close < self._prev_lower:
if close >= lower and self.Position >= 0 and self._can_trade(candle):
volume = self.Volume + self.Position if self.Position > 0 else self.Volume
self.SellMarket(volume)
self._entry_price = close
self._last_signal_time = candle.CloseTime
step_raw = self.Security.PriceStep
step = float(step_raw) if step_raw is not None else 1.0
sl = step * float(self.StopLoss)
tp = step * float(self.TakeProfit)
if self.Position > 0:
if close <= self._entry_price - sl or close >= self._entry_price + tp:
self.SellMarket(self.Position)
self._entry_price = 0.0
elif self.Position < 0:
if close >= self._entry_price + sl or close <= self._entry_price - tp:
self.BuyMarket(abs(self.Position))
self._entry_price = 0.0
self._prev_upper = upper
self._prev_lower = lower
self._prev_close = close
def OnReseted(self):
super(exp_xma_range_bands_strategy, self).OnReseted()
self._prev_upper = 0.0
self._prev_lower = 0.0
self._prev_close = 0.0
self._entry_price = 0.0
self._is_first = True
self._last_signal_time = DateTime.MinValue
def CreateClone(self):
return exp_xma_range_bands_strategy()