RSI Option Open Interest
The RSI Option Open Interest strategy is built around RSI Option Open Interest.
Testing indicates an average annual return of about 130%. It performs best in the stocks market.
Signals trigger when Option confirms trend changes on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like RsiPeriod, CandleType. Adjust these defaults to balance risk and reward.
Details
- Entry Criteria: see implementation for indicator conditions.
- Long/Short: Both directions.
- Exit Criteria: opposite signal or stop logic.
- Stops: Yes, using indicator-based calculations.
- Default Values:
RsiPeriod = 14CandleType = TimeSpan.FromMinutes(5).TimeFrame()OiPeriod = 20OiDeviationFactor = 2mStopLoss = 2m
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Option, Open, Interest
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
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>
/// RSI strategy filtered by deterministic option open-interest spikes.
/// </summary>
public class RsiWithOptionOpenInterestStrategy : Strategy
{
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _oiPeriod;
private readonly StrategyParam<decimal> _oiDeviationFactor;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<int> _cooldownBars;
private RelativeStrengthIndex _rsi = null!;
private SimpleMovingAverage _callOiSma = null!;
private SimpleMovingAverage _putOiSma = null!;
private StandardDeviation _callOiStdDev = null!;
private StandardDeviation _putOiStdDev = null!;
private decimal _currentCallOi;
private decimal _currentPutOi;
private decimal _avgCallOi;
private decimal _avgPutOi;
private decimal _stdDevCallOi;
private decimal _stdDevPutOi;
private decimal? _prevRsi;
private bool _prevCallOiSpike;
private bool _prevPutOiSpike;
private int _cooldownRemaining;
/// <summary>
/// RSI period.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Open interest averaging period.
/// </summary>
public int OiPeriod
{
get => _oiPeriod.Value;
set => _oiPeriod.Value = value;
}
/// <summary>
/// Standard deviation multiplier for OI threshold.
/// </summary>
public decimal OiDeviationFactor
{
get => _oiDeviationFactor.Value;
set => _oiDeviationFactor.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Closed candles to wait before another position change.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initialize strategy.
/// </summary>
public RsiWithOptionOpenInterestStrategy()
{
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetRange(5, 30)
.SetDisplay("RSI Period", "Period for RSI calculation", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_oiPeriod = Param(nameof(OiPeriod), 20)
.SetRange(10, 50)
.SetDisplay("OI Period", "Period for open interest averaging", "Options");
_oiDeviationFactor = Param(nameof(OiDeviationFactor), 2.5m)
.SetRange(1m, 4m)
.SetDisplay("OI StdDev Factor", "Standard deviation multiplier for OI threshold", "Options");
_stopLoss = Param(nameof(StopLoss), 2m)
.SetRange(1m, 5m)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management");
_cooldownBars = Param(nameof(CooldownBars), 18)
.SetNotNegative()
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_rsi?.Reset();
_callOiSma?.Reset();
_putOiSma?.Reset();
_callOiStdDev?.Reset();
_putOiStdDev?.Reset();
_rsi = null!;
_callOiSma = null!;
_putOiSma = null!;
_callOiStdDev = null!;
_putOiStdDev = null!;
_currentCallOi = 0m;
_currentPutOi = 0m;
_avgCallOi = 0m;
_avgPutOi = 0m;
_stdDevCallOi = 0m;
_stdDevPutOi = 0m;
_prevRsi = null;
_prevCallOiSpike = false;
_prevPutOiSpike = false;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rsi = new RelativeStrengthIndex
{
Length = RsiPeriod
};
_callOiSma = new SimpleMovingAverage
{
Length = OiPeriod
};
_callOiStdDev = new StandardDeviation
{
Length = OiPeriod
};
_putOiSma = new SimpleMovingAverage
{
Length = OiPeriod
};
_putOiStdDev = new StandardDeviation
{
Length = OiPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_rsi, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _rsi);
DrawOwnTrades(area);
}
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(StopLoss, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle, decimal rsi)
{
if (candle.State != CandleStates.Finished)
return;
SimulateOptionOi(candle);
var callOiValueSma = _callOiSma.Process(new DecimalIndicatorValue(_callOiSma, _currentCallOi, candle.OpenTime) { IsFinal = true });
var putOiValueSma = _putOiSma.Process(new DecimalIndicatorValue(_putOiSma, _currentPutOi, candle.OpenTime) { IsFinal = true });
var callOiValueStdDev = _callOiStdDev.Process(new DecimalIndicatorValue(_callOiStdDev, _currentCallOi, candle.OpenTime) { IsFinal = true });
var putOiValueStdDev = _putOiStdDev.Process(new DecimalIndicatorValue(_putOiStdDev, _currentPutOi, candle.OpenTime) { IsFinal = true });
if (!_callOiSma.IsFormed || !_putOiSma.IsFormed || !_callOiStdDev.IsFormed || !_putOiStdDev.IsFormed ||
callOiValueSma.IsEmpty || putOiValueSma.IsEmpty || callOiValueStdDev.IsEmpty || putOiValueStdDev.IsEmpty)
{
_prevRsi = rsi;
return;
}
_avgCallOi = callOiValueSma.ToDecimal();
_avgPutOi = putOiValueSma.ToDecimal();
_stdDevCallOi = callOiValueStdDev.ToDecimal();
_stdDevPutOi = putOiValueStdDev.ToDecimal();
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevRsi = rsi;
return;
}
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var callOiThreshold = _avgCallOi + (OiDeviationFactor * _stdDevCallOi);
var putOiThreshold = _avgPutOi + (OiDeviationFactor * _stdDevPutOi);
var callOiSpike = _currentCallOi > callOiThreshold;
var putOiSpike = _currentPutOi > putOiThreshold;
var callOiSpikeTransition = !_prevCallOiSpike && callOiSpike;
var putOiSpikeTransition = !_prevPutOiSpike && putOiSpike;
var oversoldCross = _prevRsi is decimal previousRsi && previousRsi >= 35m && rsi < 35m;
var overboughtCross = _prevRsi is decimal previousRsi2 && previousRsi2 <= 65m && rsi > 65m;
if (_cooldownRemaining == 0 && oversoldCross && callOiSpikeTransition && Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = CooldownBars;
}
else if (_cooldownRemaining == 0 && overboughtCross && putOiSpikeTransition && Position >= 0)
{
SellMarket(Volume + (Position > 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = CooldownBars;
}
else if (Position > 0 && rsi >= 52m)
{
SellMarket(Position);
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && rsi <= 48m)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
_prevRsi = rsi;
_prevCallOiSpike = callOiSpike;
_prevPutOiSpike = putOiSpike;
}
private void SimulateOptionOi(ICandleMessage candle)
{
var range = Math.Max(candle.HighPrice - candle.LowPrice, 1m);
var body = candle.ClosePrice - candle.OpenPrice;
var bodyRatio = Math.Abs(body) / range;
var rangeRatio = range / Math.Max(candle.OpenPrice, 1m);
var baseOi = Math.Max(candle.TotalVolume, 1m);
var spikeFactor = 1m + Math.Min(0.75m, (bodyRatio * 0.5m) + (rangeRatio * 20m));
if (body >= 0)
{
_currentCallOi = baseOi * spikeFactor;
_currentPutOi = baseOi * (0.75m + (1m - bodyRatio) * 0.25m);
}
else
{
_currentCallOi = baseOi * (0.75m + (1m - bodyRatio) * 0.25m);
_currentPutOi = baseOi * spikeFactor;
}
}
}
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, SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class rsi_with_option_open_interest_strategy(Strategy):
"""
RSI strategy filtered by deterministic option open-interest spikes.
"""
def __init__(self):
super(rsi_with_option_open_interest_strategy, self).__init__()
self._rsi_period = self.Param("RsiPeriod", 14) \
.SetRange(5, 30) \
.SetDisplay("RSI Period", "Period for RSI calculation", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._oi_period = self.Param("OiPeriod", 20) \
.SetRange(10, 50) \
.SetDisplay("OI Period", "Period for open interest averaging", "Options")
self._oi_deviation_factor = self.Param("OiDeviationFactor", 2.5) \
.SetRange(1.0, 4.0) \
.SetDisplay("OI StdDev Factor", "Standard deviation multiplier for OI threshold", "Options")
self._stop_loss = self.Param("StopLoss", 2.0) \
.SetRange(1.0, 5.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
self._cooldown_bars = self.Param("CooldownBars", 18) \
.SetNotNegative() \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General")
self._rsi = None
self._call_oi_sma = None
self._put_oi_sma = None
self._call_oi_std = None
self._put_oi_std = None
self._current_call_oi = 0.0
self._current_put_oi = 0.0
self._avg_call_oi = 0.0
self._avg_put_oi = 0.0
self._std_call_oi = 0.0
self._std_put_oi = 0.0
self._prev_rsi = None
self._prev_call_oi_spike = False
self._prev_put_oi_spike = False
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(rsi_with_option_open_interest_strategy, self).OnReseted()
self._rsi = None
self._call_oi_sma = None
self._put_oi_sma = None
self._call_oi_std = None
self._put_oi_std = None
self._current_call_oi = 0.0
self._current_put_oi = 0.0
self._avg_call_oi = 0.0
self._avg_put_oi = 0.0
self._std_call_oi = 0.0
self._std_put_oi = 0.0
self._prev_rsi = None
self._prev_call_oi_spike = False
self._prev_put_oi_spike = False
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(rsi_with_option_open_interest_strategy, self).OnStarted2(time)
oi_period = int(self._oi_period.Value)
self._rsi = RelativeStrengthIndex()
self._rsi.Length = int(self._rsi_period.Value)
self._call_oi_sma = SimpleMovingAverage()
self._call_oi_sma.Length = oi_period
self._call_oi_std = StandardDeviation()
self._call_oi_std.Length = oi_period
self._put_oi_sma = SimpleMovingAverage()
self._put_oi_sma.Length = oi_period
self._put_oi_std = StandardDeviation()
self._put_oi_std.Length = oi_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._rsi, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._rsi)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(float(self._stop_loss.Value), UnitTypes.Percent)
)
def ProcessCandle(self, candle, rsi):
if candle.State != CandleStates.Finished:
return
self.SimulateOptionOi(candle)
rsi_val = float(rsi)
call_sma_result = process_float(self._call_oi_sma, self._current_call_oi, candle.OpenTime, True)
put_sma_result = process_float(self._put_oi_sma, self._current_put_oi, candle.OpenTime, True)
call_std_result = process_float(self._call_oi_std, self._current_call_oi, candle.OpenTime, True)
put_std_result = process_float(self._put_oi_std, self._current_put_oi, candle.OpenTime, True)
if not self._call_oi_sma.IsFormed or not self._put_oi_sma.IsFormed or \
not self._call_oi_std.IsFormed or not self._put_oi_std.IsFormed or \
call_sma_result.IsEmpty or put_sma_result.IsEmpty or \
call_std_result.IsEmpty or put_std_result.IsEmpty:
self._prev_rsi = rsi_val
return
self._avg_call_oi = float(call_sma_result)
self._avg_put_oi = float(put_sma_result)
self._std_call_oi = float(call_std_result)
self._std_put_oi = float(put_std_result)
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_rsi = rsi_val
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
oi_dev = float(self._oi_deviation_factor.Value)
cooldown = int(self._cooldown_bars.Value)
call_oi_threshold = self._avg_call_oi + oi_dev * self._std_call_oi
put_oi_threshold = self._avg_put_oi + oi_dev * self._std_put_oi
call_oi_spike = self._current_call_oi > call_oi_threshold
put_oi_spike = self._current_put_oi > put_oi_threshold
call_oi_spike_transition = (not self._prev_call_oi_spike) and call_oi_spike
put_oi_spike_transition = (not self._prev_put_oi_spike) and put_oi_spike
oversold_cross = self._prev_rsi is not None and self._prev_rsi >= 35.0 and rsi_val < 35.0
overbought_cross = self._prev_rsi is not None and self._prev_rsi <= 65.0 and rsi_val > 65.0
if self._cooldown_remaining == 0 and oversold_cross and call_oi_spike_transition 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 overbought_cross and put_oi_spike_transition 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 rsi_val >= 52.0:
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
elif self.Position < 0 and rsi_val <= 48.0:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
self._prev_rsi = rsi_val
self._prev_call_oi_spike = call_oi_spike
self._prev_put_oi_spike = put_oi_spike
def SimulateOptionOi(self, candle):
range_val = max(float(candle.HighPrice - candle.LowPrice), 1.0)
body = float(candle.ClosePrice - candle.OpenPrice)
body_ratio = abs(body) / range_val
range_ratio = range_val / max(float(candle.OpenPrice), 1.0)
base_oi = max(float(candle.TotalVolume), 1.0)
spike_factor = 1.0 + min(0.75, (body_ratio * 0.5) + (range_ratio * 20.0))
if body >= 0:
self._current_call_oi = base_oi * spike_factor
self._current_put_oi = base_oi * (0.75 + (1.0 - body_ratio) * 0.25)
else:
self._current_call_oi = base_oi * (0.75 + (1.0 - body_ratio) * 0.25)
self._current_put_oi = base_oi * spike_factor
def CreateClone(self):
return rsi_with_option_open_interest_strategy()