Estrategia Keltner Reversion
Estrategia que opera en reversión a la media usando Canales de Keltner
Las pruebas indican un rendimiento anual promedio de aproximadamente 130%. Funciona mejor en el mercado de acciones.
Keltner Reversion opera contra los impulsos fuera del Canal de Keltner. Las entradas apuestan a un retorno hacia la banda media, cerrando operaciones una vez que el precio vuelve a entrar al canal o se alcanza el stop.
El ancho del canal se expande y contrae con la volatilidad, permitiendo que el sistema capture movimientos extremos mientras da espacio para que las operaciones se desarrollen. Los stops se basan típicamente en múltiplos de ATR.
Detalles
- Criterios de entrada: Señales basadas en RSI, ATR, Keltner.
- Largo/Corto: Ambos direcciones.
- Criterios de salida: Señal opuesta o stop.
- Stops: Sí.
- Valores predeterminados:
EmaPeriod= 20AtrPeriod= 14AtrMultiplier= 2.0mStopLossAtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filtros:
- Categoría: Reversión a la media
- Dirección: Ambos
- Indicadores: RSI, ATR, Keltner
- Stops: Sí
- Complejidad: Básico
- Marco temporal: Intradía (5m)
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
- Nivel de riesgo: Medio
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that trades on mean reversion using Keltner Channels.
/// Enters against outside closes confirmed by RSI, exits on return inside, protects actual fills with frozen ATR.
/// </summary>
public class KeltnerReversionStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _rsiOversold;
private readonly StrategyParam<decimal> _rsiOverbought;
private readonly StrategyParam<decimal> _stopLossAtrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private Order _pendingOrder;
private Unit _stopDistance;
private bool _protectionStarted;
/// <summary>
/// Period for EMA calculation (middle band) (default: 20)
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// Period for ATR calculation (default: 14)
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// ATR multiplier for Keltner Channel width (default: 2.0)
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Period of the RSI confirming entries (default: 14)
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// RSI level below which a long entry is confirmed (default: 30)
/// </summary>
public decimal RsiOversold
{
get => _rsiOversold.Value;
set => _rsiOversold.Value = value;
}
/// <summary>
/// RSI level above which a short entry is confirmed (default: 70)
/// </summary>
public decimal RsiOverbought
{
get => _rsiOverbought.Value;
set => _rsiOverbought.Value = value;
}
/// <summary>
/// ATR multiplier for stop-loss calculation (default: 2.0)
/// </summary>
public decimal StopLossAtrMultiplier
{
get => _stopLossAtrMultiplier.Value;
set => _stopLossAtrMultiplier.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the Keltner Reversion strategy
/// </summary>
public KeltnerReversionStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 20).SetGreaterThanZero()
.SetDisplay("EMA Period", "Period for EMA calculation (middle band)", "Technical Parameters")
.SetOptimize(10, 50, 5);
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR lookback for channel width and protection", "Technical Parameters")
.SetOptimize(7, 21, 7);
_atrMultiplier = Param(nameof(AtrMultiplier), 2.0m).SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "ATR multiplier for Keltner Channel width", "Technical Parameters")
.SetOptimize(1.0m, 3.0m, 0.5m);
_rsiPeriod = Param(nameof(RsiPeriod), 14).SetGreaterThanZero()
.SetDisplay("RSI Period", "Period of the RSI confirming entries", "Technical Parameters")
.SetOptimize(7, 21, 7);
_rsiOversold = Param(nameof(RsiOversold), 30m).SetRange(0m, 100m)
.SetDisplay("RSI Oversold", "Long entries below the lower band require RSI below this level", "Technical Parameters");
_rsiOverbought = Param(nameof(RsiOverbought), 70m).SetRange(0m, 100m)
.SetDisplay("RSI Overbought", "Short entries above the upper band require RSI above this level", "Technical Parameters");
_stopLossAtrMultiplier = Param(nameof(StopLossAtrMultiplier), 2.0m).SetNotNegative()
.SetDisplay("ATR Multiplier (Stop Loss)", "Frozen entry ATR stop distance; zero disables protection", "Risk Management")
.SetOptimize(1.0m, 3.0m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "Technical Parameters");
OrderRegistering += order => _pendingOrder = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, DataType.Level1)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
// Create subscription and bind indicators
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(ema, atr, rsi, ProcessCandle, false)
.Start();
// Configure chart
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawIndicator(area, atr);
DrawIndicator(area, rsi);
DrawOwnTrades(area);
}
}
/// <summary>
/// Process candle and check for Keltner Channel signals
/// </summary>
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before the callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue emaValue, IIndicatorValue atrValue, IIndicatorValue rsiValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Check if strategy is ready to trade
if (!emaValue.IsFormed || !atrValue.IsFormed || !rsiValue.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var ema = emaValue.GetValue<decimal>();
var atr = atrValue.GetValue<decimal>();
var rsi = rsiValue.GetValue<decimal>();
var upperBand = ema + atr * AtrMultiplier;
var lowerBand = ema - atr * AtrMultiplier;
if (Position > 0m && candle.ClosePrice >= lowerBand)
SellMarket(Position);
else if (Position < 0m && candle.ClosePrice <= upperBand)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && candle.ClosePrice < lowerBand && rsi < RsiOversold)
Enter(Sides.Buy, atr);
else if (Position == 0m && candle.ClosePrice > upperBand && rsi > RsiOverbought)
Enter(Sides.Sell, atr);
}
private void Enter(Sides side, decimal atr)
{
var distance = atr * StopLossAtrMultiplier;
_stopDistance ??= new Unit(distance);
// Update the same Unit retained by native cached controllers between flat entries.
_stopDistance.Value = distance;
if (!_protectionStarted && distance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = Volume,
Comment = "Keltner reversion entry",
});
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Subscription, Order
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class keltner_reversion_strategy(Strategy):
"""
Keltner Channel mean reversion strategy.
Buys below lower band with RSI oversold, sells above upper band with RSI overbought,
exits on return inside with ATR protection.
"""
def __init__(self):
super(keltner_reversion_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 20).SetGreaterThanZero().SetDisplay("EMA Period", "Period for EMA calculation (middle band)", "Technical Parameters")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Wilder ATR lookback for channel width and protection", "Technical Parameters")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetGreaterThanZero().SetDisplay("ATR Multiplier", "ATR multiplier for Keltner Channel width", "Technical Parameters")
self._rsi_period = self.Param("RsiPeriod", 14).SetGreaterThanZero().SetDisplay("RSI Period", "Period of the RSI confirming entries", "Technical Parameters")
self._rsi_oversold = self.Param("RsiOversold", 30.0).SetRange(0.0, 100.0).SetDisplay("RSI Oversold", "Long entries below the lower band require RSI below this level", "Technical Parameters")
self._rsi_overbought = self.Param("RsiOverbought", 70.0).SetRange(0.0, 100.0).SetDisplay("RSI Overbought", "Short entries above the upper band require RSI above this level", "Technical Parameters")
self._stop_loss_atr = self.Param("StopLossAtrMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Multiplier (Stop Loss)", "Frozen entry ATR stop distance; zero disables protection", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "Technical Parameters")
self._pending_order = None
self._stop_distance = None
self._protection_started = False
self.OrderRegistering += self._track_pending
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(keltner_reversion_strategy, self).OnReseted()
self._pending_order = None
self._stop_distance = None
self._protection_started = False
def OnStarted2(self, time):
super(keltner_reversion_strategy, self).OnStarted2(time)
ema = ExponentialMovingAverage()
ema.Length = self._ema_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(ema, atr, rsi, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawIndicator(area, atr)
self.DrawIndicator(area, rsi)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between signal bars.
pass
def _process_candle(self, candle, ema_val, atr_val, rsi_val):
if candle.State != CandleStates.Finished:
return
if not ema_val.IsFormed or not atr_val.IsFormed or not rsi_val.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
ev = ema_val.GetValue[Decimal](None)
av = atr_val.GetValue[Decimal](None)
rv = rsi_val.GetValue[Decimal](None)
mult = Decimal(self._atr_multiplier.Value)
upper = ev + av * mult
lower = ev - av * mult
close = candle.ClosePrice
if self.Position > 0 and close >= lower:
self.SellMarket(self.Position)
elif self.Position < 0 and close <= upper:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and close < lower and rv < Decimal(self._rsi_oversold.Value):
self._enter(Sides.Buy, av)
elif self.Position == 0 and close > upper and rv > Decimal(self._rsi_overbought.Value):
self._enter(Sides.Sell, av)
def _enter(self, side, atr):
distance = atr * Decimal(self._stop_loss_atr.Value)
if self._stop_distance is None:
self._stop_distance = Unit(distance)
# Update the same Unit retained by native cached controllers between flat entries.
self._stop_distance.Value = distance
if not self._protection_started and distance > 0:
self.StartProtection(Unit(), self._stop_distance, useMarketOrders=True, isLocalStop=True)
self._protection_started = True
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = self.Volume
order.Comment = "Keltner reversion entry"
self.RegisterOrder(order)
def CreateClone(self):
return keltner_reversion_strategy()