Double RSI
Double RSI uses two Relative Strength Index calculations: one on the trading chart and another on a higher timeframe. Trades are taken only when both RSI readings support the same direction, aligning short‑term entries with longer‑term momentum.
The main timeframe looks for RSI crossing out of overbought or oversold zones. If the higher‑timeframe RSI confirms the move, the strategy opens a position. An optional take‑profit can lock in gains after a predefined move.
Details
- Data: Price candles on two timeframes.
- Entry Criteria:
- Long: Lower‑timeframe RSI exits oversold AND higher‑timeframe RSI is bullish.
- Short: Lower‑timeframe RSI exits overbought AND higher‑timeframe RSI is bearish.
- Exit Criteria: Opposite RSI signal or take‑profit if
UseTPis true. - Stops: None by default.
- Default Values:
CandleType= tf(5)RSILength= 14MTFTimeframe= tf(15)UseTP= False
- Filters:
- Category: Momentum
- Direction: Long & Short
- Indicators: RSI (multi‑timeframe)
- Complexity: Moderate
- Risk level: Medium
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>
/// Double RSI strategy.
/// An RSI on the trading timeframe and another on MTFTimeframe. A long opens when the trading RSI crosses up out of
/// the oversold zone while the higher-timeframe RSI is rising (bullish); a short opens when it crosses down out of the
/// overbought zone while the higher-timeframe RSI is falling (bearish). The opposite RSI exit closes the position and an
/// optional percent take-profit locks in gains.
/// </summary>
public class DoubleRsiStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<DataType> _mtfTimeframe;
private readonly StrategyParam<decimal> _oversold;
private readonly StrategyParam<decimal> _overbought;
private readonly StrategyParam<bool> _useTp;
private readonly StrategyParam<decimal> _takeProfitPercent;
private decimal? _prevRsi;
private decimal? _mtfRsi;
private decimal? _prevMtfRsi;
/// <summary>
/// Trading candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// RSI period on both timeframes.
/// </summary>
public int RSILength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
/// <summary>
/// Higher timeframe for the confirming RSI.
/// </summary>
public DataType MTFTimeframe
{
get => _mtfTimeframe.Value;
set => _mtfTimeframe.Value = value;
}
/// <summary>
/// RSI oversold level.
/// </summary>
public decimal Oversold
{
get => _oversold.Value;
set => _oversold.Value = value;
}
/// <summary>
/// RSI overbought level.
/// </summary>
public decimal Overbought
{
get => _overbought.Value;
set => _overbought.Value = value;
}
/// <summary>
/// Enable the take-profit.
/// </summary>
public bool UseTP
{
get => _useTp.Value;
set => _useTp.Value = value;
}
/// <summary>
/// Take-profit percentage from the entry price.
/// </summary>
public decimal TakeProfitPercent
{
get => _takeProfitPercent.Value;
set => _takeProfitPercent.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public DoubleRsiStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle type", "Trading timeframe", "General");
_rsiLength = Param(nameof(RSILength), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Length", "RSI period on both timeframes", "RSI");
_mtfTimeframe = Param(nameof(MTFTimeframe), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("MTF Timeframe", "Higher timeframe for the confirming RSI", "RSI");
_oversold = Param(nameof(Oversold), 30m)
.SetDisplay("Oversold", "RSI oversold level", "RSI");
_overbought = Param(nameof(Overbought), 70m)
.SetDisplay("Overbought", "RSI overbought level", "RSI");
_useTp = Param(nameof(UseTP), false)
.SetDisplay("Use Take Profit", "Enable the percent take-profit", "Risk");
_takeProfitPercent = Param(nameof(TakeProfitPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Take Profit %", "Take-profit percentage from the entry price", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, MTFTimeframe)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevRsi = null;
_mtfRsi = null;
_prevMtfRsi = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevRsi = null;
_mtfRsi = null;
_prevMtfRsi = null;
var rsi = new RelativeStrengthIndex { Length = RSILength };
var mtfRsi = new RelativeStrengthIndex { Length = RSILength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(rsi, ProcessCandle)
.Start();
SubscribeCandles(MTFTimeframe)
.BindEx(mtfRsi, ProcessMtfCandle)
.Start();
if (UseTP)
StartProtection(new Unit(TakeProfitPercent, UnitTypes.Percent), new Unit(), useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var rsiArea = CreateChartArea();
if (rsiArea != null)
{
DrawIndicator(rsiArea, rsi);
DrawIndicator(rsiArea, mtfRsi);
}
}
}
private void ProcessMtfCandle(ICandleMessage candle, IIndicatorValue rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!rsiValue.IsFormed)
return;
_prevMtfRsi = _mtfRsi;
_mtfRsi = rsiValue.ToDecimal();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!rsiValue.IsFormed)
return;
var rsi = rsiValue.ToDecimal();
var prevRsi = _prevRsi;
_prevRsi = rsi;
if (prevRsi is not decimal lastRsi || _mtfRsi is not decimal mtfRsi || _prevMtfRsi is not decimal prevMtfRsi)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var exitsOversold = lastRsi < Oversold && rsi >= Oversold;
var exitsOverbought = lastRsi > Overbought && rsi <= Overbought;
var longSignal = exitsOversold && mtfRsi > prevMtfRsi;
var shortSignal = exitsOverbought && mtfRsi < prevMtfRsi;
if (longSignal && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (shortSignal && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
else if (Position > 0 && exitsOverbought)
SellMarket(Position);
else if (Position < 0 && exitsOversold)
BuyMarket(-Position);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class double_rsi_strategy(Strategy):
"""
Double RSI strategy.
An RSI on the trading timeframe and another on MTFTimeframe. A long opens when the trading RSI crosses up out of
the oversold zone while the higher-timeframe RSI is rising (bullish); a short opens when it crosses down out of the
overbought zone while the higher-timeframe RSI is falling (bearish). The opposite RSI exit closes the position and an
optional percent take-profit locks in gains.
"""
def __init__(self):
super(double_rsi_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle type", "Trading timeframe", "General")
self._rsi_length = self.Param("RSILength", 14) \
.SetGreaterThanZero() \
.SetDisplay("RSI Length", "RSI period on both timeframes", "RSI")
self._mtf_timeframe = self.Param("MTFTimeframe", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("MTF Timeframe", "Higher timeframe for the confirming RSI", "RSI")
self._oversold = self.Param("Oversold", 30.0) \
.SetDisplay("Oversold", "RSI oversold level", "RSI")
self._overbought = self.Param("Overbought", 70.0) \
.SetDisplay("Overbought", "RSI overbought level", "RSI")
self._use_tp = self.Param("UseTP", False) \
.SetDisplay("Use Take Profit", "Enable the percent take-profit", "Risk")
self._take_profit_percent = self.Param("TakeProfitPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit %", "Take-profit percentage from the entry price", "Risk")
self._prev_rsi = None
self._mtf_rsi = None
self._prev_mtf_rsi = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def MTFTimeframe(self):
return self._mtf_timeframe.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Security, self.MTFTimeframe)]
def OnReseted(self):
super(double_rsi_strategy, self).OnReseted()
self._prev_rsi = None
self._mtf_rsi = None
self._prev_mtf_rsi = None
def OnStarted2(self, time):
super(double_rsi_strategy, self).OnStarted2(time)
self._prev_rsi = None
self._mtf_rsi = None
self._prev_mtf_rsi = None
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_length.Value
mtf_rsi = RelativeStrengthIndex()
mtf_rsi.Length = self._rsi_length.Value
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(rsi, self._process_candle).Start()
self.SubscribeCandles(self.MTFTimeframe).BindEx(mtf_rsi, self._process_mtf_candle).Start()
if self._use_tp.Value:
self.StartProtection(Unit(Decimal(self._take_profit_percent.Value), UnitTypes.Percent), Unit(), useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
rsi_area = self.CreateChartArea()
if rsi_area is not None:
self.DrawIndicator(rsi_area, rsi)
self.DrawIndicator(rsi_area, mtf_rsi)
def _process_mtf_candle(self, candle, rsi_value):
if candle.State != CandleStates.Finished:
return
if not rsi_value.IsFormed:
return
self._prev_mtf_rsi = self._mtf_rsi
self._mtf_rsi = float(rsi_value.GetValue[Decimal](None))
def _process_candle(self, candle, rsi_value):
if candle.State != CandleStates.Finished:
return
if not rsi_value.IsFormed:
return
rsi = float(rsi_value.GetValue[Decimal](None))
prev_rsi = self._prev_rsi
self._prev_rsi = rsi
if prev_rsi is None or self._mtf_rsi is None or self._prev_mtf_rsi is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
oversold = float(self._oversold.Value)
overbought = float(self._overbought.Value)
exits_oversold = prev_rsi < oversold and rsi >= oversold
exits_overbought = prev_rsi > overbought and rsi <= overbought
long_signal = exits_oversold and self._mtf_rsi > self._prev_mtf_rsi
short_signal = exits_overbought and self._mtf_rsi < self._prev_mtf_rsi
if long_signal and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif short_signal and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif self.Position > 0 and exits_overbought:
self.SellMarket(self.Position)
elif self.Position < 0 and exits_oversold:
self.BuyMarket(-self.Position)
def CreateClone(self):
return double_rsi_strategy()