RSI + 1200 ищет развороты тренда, подтверждённые силой движения и
трендовым фильтром из старшего тайм‑фрейма. В стратегии используется классический
RSI с периодом 14 и экспоненциальная скользящая средняя, рассчитанная по
120‑минутным свечам. Сигналы на вход формируются только тогда, когда импульс и
направление тренда совпадают.
Историческое тестирование на ликвидных криптовалютных парах показывает, что
стратегия работает лучше всего в устойчивых направленных движениях. Во время
флэта возможны ложные сигналы, поэтому вокруг EMA используется небольшой
допуск, а риски ограничиваются процентным стоп‑лоссом.
Длинная позиция открывается, когда RSI выходит из зоны перепроданности и цена
находится не более чем на один процент выше EMA старшего тайм‑фрейма. Для
короткой позиции выполняются зеркальные условия. Закрытие осуществляется при
достижении RSI противоположного экстремума, что сигнализирует об исчерпании
движения. Дополнительно выставляется стоп на расстоянии stopLossPercent процентов
от цены входа.
Подробности
Условия входа
Лонг: RSI пересекает rsiOversold снизу вверх и цена ≤ 1% выше EMA.
Шорт: RSI пересекает rsiOverbought сверху вниз и цена ≥ 1% ниже EMA.
Условия выхода
Лонг: RSI поднимается выше rsiOverbought.
Шорт: RSI опускается ниже rsiOversold.
Стопы: процентный стоп‑лосс stopLossPercent.
Параметры по умолчанию
rsiLength = 14
rsiOverbought = 72
rsiOversold = 28
emaLength = 150
mtfTimeframe = 120 минут
stopLossPercent = 0.10 (10%)
Фильтры
Категория: Следование тренду
Направление: оба
Индикаторы: RSI, EMA
Стопы: есть
Сложность: средняя
Тайм‑фрейм: внутридневной / мульти‑таймфрейм
Сезонность: нет
Нейросети: нет
Дивергенции: нет
Уровень риска: умеренный
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// RSI + 1200 Strategy.
/// Uses RSI crossover signals with EMA trend filter.
/// Buys when RSI crosses above oversold level while price is above EMA.
/// Sells when RSI crosses below overbought level while price is below EMA.
/// </summary>
public class RsiPlus1200Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<int> _rsiOverbought;
private readonly StrategyParam<int> _rsiOversold;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<int> _cooldownBars;
private RelativeStrengthIndex _rsi;
private ExponentialMovingAverage _ema;
private decimal _prevRsi;
private int _cooldownRemaining;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
public int RsiOverbought
{
get => _rsiOverbought.Value;
set => _rsiOverbought.Value = value;
}
public int RsiOversold
{
get => _rsiOversold.Value;
set => _rsiOversold.Value = value;
}
public int EmaLength
{
get => _emaLength.Value;
set => _emaLength.Value = value;
}
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
public RsiPlus1200Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_rsiLength = Param(nameof(RsiLength), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Length", "RSI calculation length", "RSI");
_rsiOverbought = Param(nameof(RsiOverbought), 70)
.SetDisplay("RSI Overbought", "RSI overbought level", "RSI");
_rsiOversold = Param(nameof(RsiOversold), 30)
.SetDisplay("RSI Oversold", "RSI oversold level", "RSI");
_emaLength = Param(nameof(EmaLength), 100)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "EMA period for trend filter", "Moving Average");
_cooldownBars = Param(nameof(CooldownBars), 10)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_rsi = null;
_ema = null;
_prevRsi = 0;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rsi = new RelativeStrengthIndex { Length = RsiLength };
_ema = new ExponentialMovingAverage { Length = EmaLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_rsi, _ema, OnProcess)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
}
private void OnProcess(ICandleMessage candle, decimal rsiVal, decimal emaVal)
{
if (candle.State != CandleStates.Finished)
return;
if (!_rsi.IsFormed || !_ema.IsFormed)
{
_prevRsi = rsiVal;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevRsi = rsiVal;
return;
}
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
_prevRsi = rsiVal;
return;
}
if (_prevRsi == 0)
{
_prevRsi = rsiVal;
return;
}
// RSI crossovers
var rsiCrossUpOversold = rsiVal > RsiOversold && _prevRsi <= RsiOversold;
var rsiCrossDownOverbought = rsiVal < RsiOverbought && _prevRsi >= RsiOverbought;
// Buy: RSI crosses above oversold + price above EMA (uptrend)
if (rsiCrossUpOversold && candle.ClosePrice > emaVal && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Sell: RSI crosses below overbought + price below EMA (downtrend)
else if (rsiCrossDownOverbought && candle.ClosePrice < emaVal && Position >= 0)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Exit long: RSI overbought
else if (Position > 0 && rsiVal > RsiOverbought)
{
SellMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
// Exit short: RSI oversold
else if (Position < 0 && rsiVal < RsiOversold)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
_prevRsi = rsiVal;
}
}
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
from StockSharp.Algo.Indicators import RelativeStrengthIndex, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class rsi_plus_1200_strategy(Strategy):
"""RSI + 1200 Strategy."""
def __init__(self):
super(rsi_plus_1200_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._rsi_length = self.Param("RsiLength", 14) \
.SetDisplay("RSI Length", "RSI calculation length", "RSI")
self._rsi_overbought = self.Param("RsiOverbought", 70) \
.SetDisplay("RSI Overbought", "RSI overbought level", "RSI")
self._rsi_oversold = self.Param("RsiOversold", 30) \
.SetDisplay("RSI Oversold", "RSI oversold level", "RSI")
self._ema_length = self.Param("EmaLength", 100) \
.SetDisplay("EMA Length", "EMA period for trend filter", "Moving Average")
self._cooldown_bars = self.Param("CooldownBars", 10) \
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk")
self._rsi = None
self._ema = None
self._prev_rsi = 0.0
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(rsi_plus_1200_strategy, self).OnReseted()
self._rsi = None
self._ema = None
self._prev_rsi = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(rsi_plus_1200_strategy, self).OnStarted2(time)
self._rsi = RelativeStrengthIndex()
self._rsi.Length = int(self._rsi_length.Value)
self._ema = ExponentialMovingAverage()
self._ema.Length = int(self._ema_length.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._rsi, self._ema, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ema)
self.DrawOwnTrades(area)
def _on_process(self, candle, rsi_val, ema_val):
if candle.State != CandleStates.Finished:
return
if not self._rsi.IsFormed or not self._ema.IsFormed:
self._prev_rsi = float(rsi_val)
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_rsi = float(rsi_val)
return
rsi = float(rsi_val)
ema = float(ema_val)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._prev_rsi = rsi
return
if self._prev_rsi == 0.0:
self._prev_rsi = rsi
return
rsi_ob = int(self._rsi_overbought.Value)
rsi_os = int(self._rsi_oversold.Value)
cooldown = int(self._cooldown_bars.Value)
close = float(candle.ClosePrice)
rsi_cross_up_oversold = rsi > rsi_os and self._prev_rsi <= rsi_os
rsi_cross_down_overbought = rsi < rsi_ob and self._prev_rsi >= rsi_ob
if rsi_cross_up_oversold and close > ema and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
self._cooldown_remaining = cooldown
elif rsi_cross_down_overbought and close < ema and self.Position >= 0:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
self._cooldown_remaining = cooldown
elif self.Position > 0 and rsi > rsi_ob:
self.SellMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
elif self.Position < 0 and rsi < rsi_os:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
self._prev_rsi = rsi
def CreateClone(self):
return rsi_plus_1200_strategy()