Адаптивный RSI с фильтром объёма
Стратегия Adaptive RSI Volume Filter торгует по адаптивному RSI с подтверждением объёмом. Сигналы формируются, когда индикаторы подтверждают отфильтрованные входы на внутридневных данных (5м). Такой подход подходит активным трейдерам. Стопы рассчитываются исходя из кратных ATR и параметров MinRsiPeriod, MaxRsiPeriod. Эти значения можно изменять для баланса риска и прибыли.
Тестирование показывает среднегодичную доходность около 106%. Стратегию лучше запускать на фондовом рынке.
Подробности
- Условия входа: см. реализацию для условий по индикаторам.
- Длинные/короткие позиции: обе стороны.
- Условия выхода: обратный сигнал или логика стопов.
- Стопы: да, вычисляются на основе индикаторов.
- Значения по умолчанию:
MinRsiPeriod = 10MaxRsiPeriod = 20AtrPeriod = 14VolumeLookback = 20CandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Фильтры:
- Категория: Следование за трендом
- Направление: Оба
- Индикаторы: multiple indicators
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Внутридневной (5m)
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
using System;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that trades an ATR-adaptive RSI confirmed by relative volume.
/// </summary>
public class AdaptiveRsiVolumeStrategy : Strategy
{
private readonly StrategyParam<int> _minRsiPeriod;
private readonly StrategyParam<int> _maxRsiPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<int> _volumeLookback;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private RelativeStrengthIndex _fastRsi = null!;
private RelativeStrengthIndex _slowRsi = null!;
private AverageTrueRange _atr = null!;
private SimpleMovingAverage _volumeSma = null!;
private decimal _adaptiveRsiValue;
private decimal _avgVolume;
private decimal _atrValue;
private int _cooldownRemaining;
/// <summary>
/// Strategy parameter: Minimum RSI period.
/// </summary>
public int MinRsiPeriod
{
get => _minRsiPeriod.Value;
set => _minRsiPeriod.Value = value;
}
/// <summary>
/// Strategy parameter: Maximum RSI period.
/// </summary>
public int MaxRsiPeriod
{
get => _maxRsiPeriod.Value;
set => _maxRsiPeriod.Value = value;
}
/// <summary>
/// Strategy parameter: ATR period for volatility normalization.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Strategy parameter: Volume lookback period.
/// </summary>
public int VolumeLookback
{
get => _volumeLookback.Value;
set => _volumeLookback.Value = value;
}
/// <summary>
/// Strategy parameter: Closed candles to wait between position changes.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Strategy parameter: Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public AdaptiveRsiVolumeStrategy()
{
_minRsiPeriod = Param(nameof(MinRsiPeriod), 8)
.SetGreaterThanZero()
.SetDisplay("Min RSI Period", "Fast RSI period used in high volatility", "Indicator Settings");
_maxRsiPeriod = Param(nameof(MaxRsiPeriod), 21)
.SetGreaterThanZero()
.SetDisplay("Max RSI Period", "Slow RSI period used in low volatility", "Indicator Settings");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Period for ATR volatility calculation", "Indicator Settings");
_volumeLookback = Param(nameof(VolumeLookback), 12)
.SetGreaterThanZero()
.SetDisplay("Volume Lookback", "Periods used for average volume", "Volume Settings");
_cooldownBars = Param(nameof(CooldownBars), 8)
.SetNotNegative()
.SetDisplay("Cooldown Bars", "Closed candles to wait before another signal", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).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();
_adaptiveRsiValue = 50m;
_avgVolume = 0m;
_atrValue = 0m;
_cooldownRemaining = 0;
_fastRsi?.Reset();
_slowRsi?.Reset();
_atr?.Reset();
_volumeSma?.Reset();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastRsi = new RelativeStrengthIndex
{
Length = MinRsiPeriod
};
_slowRsi = new RelativeStrengthIndex
{
Length = MaxRsiPeriod
};
_atr = new AverageTrueRange
{
Length = AtrPeriod
};
_volumeSma = new SimpleMovingAverage
{
Length = VolumeLookback
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastRsi);
DrawIndicator(area, _slowRsi);
DrawOwnTrades(area);
}
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var fastRsiValue = _fastRsi.Process(new DecimalIndicatorValue(_fastRsi, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
var slowRsiValue = _slowRsi.Process(new DecimalIndicatorValue(_slowRsi, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
var atrValue = _atr.Process(new CandleIndicatorValue(_atr, candle) { IsFinal = true });
var volumeValue = _volumeSma.Process(new DecimalIndicatorValue(_volumeSma, candle.TotalVolume, candle.OpenTime) { IsFinal = true });
if (!_fastRsi.IsFormed || !_slowRsi.IsFormed || !_atr.IsFormed || !_volumeSma.IsFormed ||
fastRsiValue.IsEmpty || slowRsiValue.IsEmpty || atrValue.IsEmpty || volumeValue.IsEmpty)
return;
_avgVolume = volumeValue.ToDecimal();
_atrValue = atrValue.ToDecimal();
var fastRsi = fastRsiValue.ToDecimal();
var slowRsi = slowRsiValue.ToDecimal();
var normalizedAtr = Math.Min(Math.Max(_atrValue / Math.Max(candle.ClosePrice * 0.02m, 1m), 0m), 1m);
// High volatility shifts weight to the faster RSI.
_adaptiveRsiValue = slowRsi + ((fastRsi - slowRsi) * normalizedAtr);
if (!IsFormedAndOnlineAndAllowTrading())
return;
var isHighVolume = candle.TotalVolume >= (_avgVolume * 0.9m);
var oversoldLevel = 45m - (normalizedAtr * 5m);
var overboughtLevel = 55m + (normalizedAtr * 5m);
if (_cooldownRemaining == 0 && isHighVolume && _adaptiveRsiValue <= oversoldLevel && Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = CooldownBars;
}
else if (_cooldownRemaining == 0 && isHighVolume && _adaptiveRsiValue >= overboughtLevel && Position >= 0)
{
SellMarket(Volume + (Position > 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = CooldownBars;
}
else if (Position > 0 && _adaptiveRsiValue >= 52m)
{
SellMarket(Position);
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && _adaptiveRsiValue <= 48m)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
}
}
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, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import RelativeStrengthIndex, AverageTrueRange, SimpleMovingAverage, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class adaptive_rsi_volume_strategy(Strategy):
"""
Strategy that trades an ATR-adaptive RSI confirmed by relative volume.
"""
def __init__(self):
super(adaptive_rsi_volume_strategy, self).__init__()
self._min_rsi_period = self.Param("MinRsiPeriod", 8) \
.SetGreaterThanZero() \
.SetDisplay("Min RSI Period", "Fast RSI period used in high volatility", "Indicator Settings")
self._max_rsi_period = self.Param("MaxRsiPeriod", 21) \
.SetGreaterThanZero() \
.SetDisplay("Max RSI Period", "Slow RSI period used in low volatility", "Indicator Settings")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "Period for ATR volatility calculation", "Indicator Settings")
self._volume_lookback = self.Param("VolumeLookback", 12) \
.SetGreaterThanZero() \
.SetDisplay("Volume Lookback", "Periods used for average volume", "Volume Settings")
self._cooldown_bars = self.Param("CooldownBars", 8) \
.SetNotNegative() \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another signal", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._fast_rsi = None
self._slow_rsi = None
self._atr = None
self._volume_sma = None
self._adaptive_rsi_value = 50.0
self._avg_volume = 0.0
self._atr_value = 0.0
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(adaptive_rsi_volume_strategy, self).OnReseted()
self._adaptive_rsi_value = 50.0
self._avg_volume = 0.0
self._atr_value = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(adaptive_rsi_volume_strategy, self).OnStarted2(time)
self._fast_rsi = RelativeStrengthIndex()
self._fast_rsi.Length = int(self._min_rsi_period.Value)
self._slow_rsi = RelativeStrengthIndex()
self._slow_rsi.Length = int(self._max_rsi_period.Value)
self._atr = AverageTrueRange()
self._atr.Length = int(self._atr_period.Value)
self._volume_sma = SimpleMovingAverage()
self._volume_sma.Length = int(self._volume_lookback.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._fast_rsi)
self.DrawIndicator(area, self._slow_rsi)
self.DrawOwnTrades(area)
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent)
)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
fast_rsi_val = process_float(self._fast_rsi, candle.ClosePrice, candle.OpenTime, True)
slow_rsi_val = process_float(self._slow_rsi, candle.ClosePrice, candle.OpenTime, True)
aiv = CandleIndicatorValue(self._atr, candle)
aiv.IsFinal = True
atr_val = self._atr.Process(aiv)
volume_val = process_float(self._volume_sma, candle.TotalVolume, candle.OpenTime, True)
if not self._fast_rsi.IsFormed or not self._slow_rsi.IsFormed or not self._atr.IsFormed or not self._volume_sma.IsFormed:
return
if fast_rsi_val.IsEmpty or slow_rsi_val.IsEmpty or atr_val.IsEmpty or volume_val.IsEmpty:
return
self._avg_volume = float(volume_val)
self._atr_value = float(atr_val)
fast_rsi = float(fast_rsi_val)
slow_rsi = float(slow_rsi_val)
close_price = float(candle.ClosePrice)
normalized_atr = min(max(self._atr_value / max(close_price * 0.02, 1.0), 0.0), 1.0)
self._adaptive_rsi_value = slow_rsi + ((fast_rsi - slow_rsi) * normalized_atr)
if not self.IsFormedAndOnlineAndAllowTrading():
return
total_volume = float(candle.TotalVolume)
is_high_volume = total_volume >= (self._avg_volume * 0.9)
oversold_level = 45.0 - (normalized_atr * 5.0)
overbought_level = 55.0 + (normalized_atr * 5.0)
cooldown = int(self._cooldown_bars.Value)
if self._cooldown_remaining == 0 and is_high_volume and self._adaptive_rsi_value <= oversold_level and self.Position <= 0:
vol = self.Volume
if self.Position < 0:
vol = self.Volume + Math.Abs(self.Position)
self.BuyMarket(vol)
self._cooldown_remaining = cooldown
elif self._cooldown_remaining == 0 and is_high_volume and self._adaptive_rsi_value >= overbought_level and self.Position >= 0:
vol = self.Volume
if self.Position > 0:
vol = self.Volume + Math.Abs(self.Position)
self.SellMarket(vol)
self._cooldown_remaining = cooldown
elif self.Position > 0 and self._adaptive_rsi_value >= 52.0:
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
elif self.Position < 0 and self._adaptive_rsi_value <= 48.0:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
def CreateClone(self):
return adaptive_rsi_volume_strategy()