IBS Internal Bar Strength
IBS Internal Bar Strength is a mean-reversion strategy that uses the previous bar's close within its range to find oversold or overbought conditions. An optional EMA filter aligns trades with the trend and entries can be added only when price moves a minimum percentage from the last entry. Positions exit when IBS crosses the opposite threshold or a maximum holding time is reached.
Details
- Data: Price candles.
- Entry Criteria:
- Long: IBS below entry threshold, EMA condition met, and allowed direction.
- Short: IBS above exit threshold, EMA condition met, and allowed direction.
- Exit Criteria: IBS crossing opposite threshold or trade duration limit.
- Stops: Time-based exit.
- Default Values:
IbsEntryThreshold= 0.09IbsExitThreshold= 0.985EmaPeriod= 220MinEntryPct= 0MaxTradeDuration= 14
- Filters:
- Category: Mean reversion
- Direction: Long & Short
- Indicators: IBS, EMA
- Complexity: Low
- Risk level: Medium
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>
/// Previous-bar IBS mean reversion with optional EMA alignment, spaced additions,
/// full-basket opposite-threshold exits and a maximum holding period in finished bars.
/// </summary>
public class IbsInternalBarStrengthStrategy : Strategy
{
private readonly StrategyParam<decimal> _ibsEntryThreshold;
private readonly StrategyParam<decimal> _ibsExitThreshold;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<bool> _useEmaFilter;
private readonly StrategyParam<bool> _allowLong;
private readonly StrategyParam<bool> _allowShort;
private readonly StrategyParam<decimal> _minEntryPct;
private readonly StrategyParam<int> _maxTradeDuration;
private readonly StrategyParam<DataType> _candleType;
private decimal? _previousIbs;
private decimal _previousClose;
private decimal _previousEma;
private bool _previousEmaReady;
private decimal _lastEntryPrice;
private int _barCount;
private int _firstEntryBar;
public decimal IbsEntryThreshold { get => _ibsEntryThreshold.Value; set => _ibsEntryThreshold.Value = value; }
public decimal IbsExitThreshold { get => _ibsExitThreshold.Value; set => _ibsExitThreshold.Value = value; }
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public bool UseEmaFilter { get => _useEmaFilter.Value; set => _useEmaFilter.Value = value; }
public bool AllowLong { get => _allowLong.Value; set => _allowLong.Value = value; }
public bool AllowShort { get => _allowShort.Value; set => _allowShort.Value = value; }
public decimal MinEntryPct { get => _minEntryPct.Value; set => _minEntryPct.Value = value; }
public int MaxTradeDuration { get => _maxTradeDuration.Value; set => _maxTradeDuration.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public IbsInternalBarStrengthStrategy()
{
_ibsEntryThreshold = Param(nameof(IbsEntryThreshold), 0.09m).SetRange(0m, 1m);
_ibsExitThreshold = Param(nameof(IbsExitThreshold), 0.985m).SetRange(0m, 1m);
_emaPeriod = Param(nameof(EmaPeriod), 220).SetGreaterThanZero();
_useEmaFilter = Param(nameof(UseEmaFilter), true);
_allowLong = Param(nameof(AllowLong), true);
_allowShort = Param(nameof(AllowShort), true);
_minEntryPct = Param(nameof(MinEntryPct), 0m).SetNotNegative();
_maxTradeDuration = Param(nameof(MaxTradeDuration), 14).SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (IbsEntryThreshold >= IbsExitThreshold)
throw new InvalidOperationException("IbsEntryThreshold must be below IbsExitThreshold.");
ResetState();
// A disabled filter must not impose a 220-bar warm-up on the IBS signal.
var ema = new ExponentialMovingAverage { Length = UseEmaFilter ? EmaPeriod : 1 };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ema, (candle, value) => ProcessCandle(candle, value, ema.IsFormed)).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
if (UseEmaFilter)
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ResetState()
{
_previousIbs = null;
_previousClose = _previousEma = _lastEntryPrice = 0m;
_previousEmaReady = false;
_barCount = _firstEntryBar = 0;
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue, bool emaReady)
{
if (candle.State != CandleStates.Finished)
return;
var ibs = _previousIbs;
var previousClose = _previousClose;
var previousEma = _previousEma;
var filterReady = _previousEmaReady;
var range = candle.HighPrice - candle.LowPrice;
_previousIbs = range == 0m ? 0.5m : (candle.ClosePrice - candle.LowPrice) / range;
_previousClose = candle.ClosePrice;
_previousEma = emaValue;
_previousEmaReady = emaReady;
_barCount++;
if (ibs is null || !IsFormedAndOnlineAndAllowTrading())
return;
if (Position != 0m)
{
var oppositeThreshold = Position > 0m ? ibs > IbsExitThreshold : ibs < IbsEntryThreshold;
if (oppositeThreshold || _barCount - _firstEntryBar >= MaxTradeDuration)
{
if (Position > 0m)
SellMarket(Math.Abs(Position));
else
BuyMarket(Math.Abs(Position));
_lastEntryPrice = 0m;
_firstEntryBar = 0;
return;
}
}
if (UseEmaFilter && !filterReady)
return;
if (Position != 0m && _lastEntryPrice > 0m
&& 100m * Math.Abs(candle.ClosePrice - _lastEntryPrice) / _lastEntryPrice < MinEntryPct)
return;
var buy = AllowLong && Position >= 0m && ibs < IbsEntryThreshold
&& (!UseEmaFilter || previousClose > previousEma);
var sell = AllowShort && Position <= 0m && ibs > IbsExitThreshold
&& (!UseEmaFilter || previousClose < previousEma);
if (!buy && !sell)
return;
// Added entries do not extend the lifetime of the existing basket.
if (Position == 0m)
_firstEntryBar = _barCount;
_lastEntryPrice = candle.ClosePrice;
if (buy)
BuyMarket();
else
SellMarket();
}
}
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, Convert, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class ibs_internal_bar_strength_strategy(Strategy):
"""Previous-bar IBS, optional EMA trend filter, spaced additions and full-basket exits."""
def __init__(self):
super(ibs_internal_bar_strength_strategy, self).__init__()
self._ibs_entry_threshold = self.Param("IbsEntryThreshold", 0.09).SetRange(0.0, 1.0)
self._ibs_exit_threshold = self.Param("IbsExitThreshold", 0.985).SetRange(0.0, 1.0)
self._ema_period = self.Param("EmaPeriod", 220).SetGreaterThanZero()
self._use_ema_filter = self.Param("UseEmaFilter", True)
self._allow_long = self.Param("AllowLong", True)
self._allow_short = self.Param("AllowShort", True)
self._min_entry_pct = self.Param("MinEntryPct", 0.0).SetNotNegative()
self._max_trade_duration = self.Param("MaxTradeDuration", 14).SetGreaterThanZero()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._reset_state()
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value)]
def OnReseted(self):
super(ibs_internal_bar_strength_strategy, self).OnReseted()
self._reset_state()
def _reset_state(self):
self._previous_ibs = None
self._previous_close = Decimal.Zero
self._previous_ema = Decimal.Zero
self._previous_ema_ready = False
self._last_entry_price = Decimal.Zero
self._bar_count = 0
self._first_entry_bar = 0
def OnStarted2(self, time):
super(ibs_internal_bar_strength_strategy, self).OnStarted2(time)
if self._ibs_entry_threshold.Value >= self._ibs_exit_threshold.Value:
raise ValueError("IbsEntryThreshold must be below IbsExitThreshold.")
self._reset_state()
ema = ExponentialMovingAverage()
ema.Length = self._ema_period.Value if self._use_ema_filter.Value else 1
def process(candle, value):
self._process_candle(candle, value, ema.IsFormed)
subscription = self.SubscribeCandles(self._candle_type.Value)
subscription.Bind(ema, process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
if self._use_ema_filter.Value:
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ema_value, ema_ready):
if candle.State != CandleStates.Finished:
return
ibs = self._previous_ibs
previous_close = self._previous_close
previous_ema = self._previous_ema
filter_ready = self._previous_ema_ready
candle_range = candle.HighPrice - candle.LowPrice
self._previous_ibs = (Convert.ToDecimal(0.5) if candle_range == 0 else
(candle.ClosePrice - candle.LowPrice) / candle_range)
self._previous_close = candle.ClosePrice
self._previous_ema = ema_value
self._previous_ema_ready = ema_ready
self._bar_count += 1
if ibs is None or not self.IsFormedAndOnlineAndAllowTrading():
return
entry_threshold = Convert.ToDecimal(self._ibs_entry_threshold.Value)
exit_threshold = Convert.ToDecimal(self._ibs_exit_threshold.Value)
if self.Position != 0:
opposite_threshold = ibs > exit_threshold if self.Position > 0 else ibs < entry_threshold
if (opposite_threshold or
self._bar_count - self._first_entry_bar >= self._max_trade_duration.Value):
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
else:
self.BuyMarket(Math.Abs(self.Position))
self._last_entry_price = Decimal.Zero
self._first_entry_bar = 0
return
if self._use_ema_filter.Value and not filter_ready:
return
if (self.Position != 0 and self._last_entry_price > 0 and
Decimal(100) * Math.Abs(candle.ClosePrice - self._last_entry_price) / self._last_entry_price <
Convert.ToDecimal(self._min_entry_pct.Value)):
return
buy = (self._allow_long.Value and self.Position >= 0 and ibs < entry_threshold and
(not self._use_ema_filter.Value or previous_close > previous_ema))
sell = (self._allow_short.Value and self.Position <= 0 and ibs > exit_threshold and
(not self._use_ema_filter.Value or previous_close < previous_ema))
if not buy and not sell:
return
if self.Position == 0:
self._first_entry_bar = self._bar_count
self._last_entry_price = candle.ClosePrice
if buy:
self.BuyMarket()
else:
self.SellMarket()
def CreateClone(self):
return ibs_internal_bar_strength_strategy()