Altarius RSI Stochastic Dual Strategy
Overview
Altarius RSI Stochastic Dual Strategy is a conversion of the MetaTrader expert advisor AltariusRSIxampnSTOH. The logic combines two stochastic oscillators with a short-period RSI filter. The slow stochastic identifies trend direction and overbought/oversold zones, while the fast stochastic measures momentum strength. Exits rely on RSI and the slow stochastic signal line to trail winning trades and cut losses. Additional money management features mirror the original MQL logic by reducing position size after losses and enforcing an equity drawdown limit.
Trading Logic
- Data Source – The strategy works on configurable candles (default 15-minute bars). All calculations use candle close data.
- Entry Conditions
- Long setup: Slow stochastic main line (15,8,8) is above its signal line yet still below the
BuyStochasticLimit(50 by default). Fast stochastic (10,3,3) shows momentum with an absolute difference between main and signal lines above theStochasticDifferenceThreshold(5 by default). - Short setup: Slow stochastic main line is below its signal line but remains above the
SellStochasticLimit(55 by default). The fast stochastic must again show a difference greater than the momentum threshold.
- Long setup: Slow stochastic main line (15,8,8) is above its signal line yet still below the
- Exit Conditions
- Long exit: Triggered when the RSI (period 4) exceeds
ExitRsiHigh(60) and the slow stochastic signal line declines below its previous value while staying aboveExitStochasticHigh(70). - Short exit: Triggered when the RSI drops under
ExitRsiLow(40) and the slow stochastic signal line rises above its previous value while staying belowExitStochasticLow(30). - Risk exit: If floating PnL drops below the allowed equity drawdown (
MaximumRiskPercent), all positions are flattened immediately.
- Long exit: Triggered when the RSI (period 4) exceeds
- Position Sizing – Starts with
BaseVolumeand reduces the effective size after consecutive losing trades viaDecreaseFactor. Broker volume constraints are respected using the security volume step and limits.
Parameters
| Parameter | Description |
|---|---|
BaseVolume |
Base order size before risk management adjustments. |
MaximumRiskPercent |
Percentage of account equity that can be lost before the strategy forcefully closes positions. |
DecreaseFactor |
Divider controlling how quickly position size contracts after consecutive losses. |
RsiPeriod |
RSI length used for exit decisions. |
SlowStochasticPeriod, SlowStochasticK, SlowStochasticD |
Configuration for the slow stochastic oscillator that drives trend direction. |
FastStochasticPeriod, FastStochasticK, FastStochasticD |
Configuration for the fast stochastic oscillator that measures momentum. |
StochasticDifferenceThreshold |
Minimum distance between fast stochastic main and signal lines to confirm momentum. |
BuyStochasticLimit, SellStochasticLimit |
Slow stochastic levels that define the acceptable trading zone for new positions. |
ExitRsiHigh, ExitRsiLow |
RSI levels that prepare long or short exits. |
ExitStochasticHigh, ExitStochasticLow |
Slow stochastic signal levels that finalize exits. |
CandleType |
Candle data source for indicator calculations. |
Notes
- The strategy trades a single position at a time, mirroring the original expert advisor behaviour.
- Volume adjustments and drawdown protection are calculated using the current portfolio information available in StockSharp.
- Chart visualization draws candles, both stochastic oscillators, and trade markers when a chart area is available.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy converted from AltariusRSIxampnSTOH MQL4 expert advisor.
/// Combines dual stochastic filters with RSI based exits and dynamic position sizing.
/// </summary>
public class AltariusRsiStochasticDualStrategy : Strategy
{
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _maximumRiskPercent;
private readonly StrategyParam<decimal> _decreaseFactor;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<int> _slowStochPeriod;
private readonly StrategyParam<int> _slowStochK;
private readonly StrategyParam<int> _slowStochD;
private readonly StrategyParam<int> _fastStochPeriod;
private readonly StrategyParam<int> _fastStochK;
private readonly StrategyParam<int> _fastStochD;
private readonly StrategyParam<decimal> _differenceThreshold;
private readonly StrategyParam<decimal> _buyLimit;
private readonly StrategyParam<decimal> _sellLimit;
private readonly StrategyParam<decimal> _exitRsiHigh;
private readonly StrategyParam<decimal> _exitRsiLow;
private readonly StrategyParam<decimal> _exitStochHigh;
private readonly StrategyParam<decimal> _exitStochLow;
private readonly StrategyParam<DataType> _candleType;
private decimal _previousSlowSignal;
private bool _hasPreviousSlowSignal;
private decimal _lastRealizedPnL;
private int _consecutiveLosses;
/// <summary>
/// Base order volume before risk and loss adjustments.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Maximum share of account equity allowed to be lost before forcing an exit.
/// </summary>
public decimal MaximumRiskPercent
{
get => _maximumRiskPercent.Value;
set => _maximumRiskPercent.Value = value;
}
/// <summary>
/// Factor controlling how quickly the volume shrinks after consecutive losses.
/// </summary>
public decimal DecreaseFactor
{
get => _decreaseFactor.Value;
set => _decreaseFactor.Value = value;
}
/// <summary>
/// Period for the RSI exit filter.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Period for the slow stochastic oscillator used to open trades.
/// </summary>
public int SlowStochasticPeriod
{
get => _slowStochPeriod.Value;
set => _slowStochPeriod.Value = value;
}
/// <summary>
/// %K smoothing length for the slow stochastic.
/// </summary>
public int SlowStochasticK
{
get => _slowStochK.Value;
set => _slowStochK.Value = value;
}
/// <summary>
/// %D smoothing length for the slow stochastic.
/// </summary>
public int SlowStochasticD
{
get => _slowStochD.Value;
set => _slowStochD.Value = value;
}
/// <summary>
/// Period for the fast stochastic oscillator used as momentum filter.
/// </summary>
public int FastStochasticPeriod
{
get => _fastStochPeriod.Value;
set => _fastStochPeriod.Value = value;
}
/// <summary>
/// %K smoothing length for the fast stochastic.
/// </summary>
public int FastStochasticK
{
get => _fastStochK.Value;
set => _fastStochK.Value = value;
}
/// <summary>
/// %D smoothing length for the fast stochastic.
/// </summary>
public int FastStochasticD
{
get => _fastStochD.Value;
set => _fastStochD.Value = value;
}
/// <summary>
/// Minimum distance between fast stochastic main and signal lines to allow entries.
/// </summary>
public decimal StochasticDifferenceThreshold
{
get => _differenceThreshold.Value;
set => _differenceThreshold.Value = value;
}
/// <summary>
/// Upper bound on the slow stochastic main line when opening long trades.
/// </summary>
public decimal BuyStochasticLimit
{
get => _buyLimit.Value;
set => _buyLimit.Value = value;
}
/// <summary>
/// Lower bound on the slow stochastic main line when opening short trades.
/// </summary>
public decimal SellStochasticLimit
{
get => _sellLimit.Value;
set => _sellLimit.Value = value;
}
/// <summary>
/// RSI threshold that triggers exit for long positions.
/// </summary>
public decimal ExitRsiHigh
{
get => _exitRsiHigh.Value;
set => _exitRsiHigh.Value = value;
}
/// <summary>
/// RSI threshold that triggers exit for short positions.
/// </summary>
public decimal ExitRsiLow
{
get => _exitRsiLow.Value;
set => _exitRsiLow.Value = value;
}
/// <summary>
/// Stochastic level that confirms the exit of long positions.
/// </summary>
public decimal ExitStochasticHigh
{
get => _exitStochHigh.Value;
set => _exitStochHigh.Value = value;
}
/// <summary>
/// Stochastic level that confirms the exit of short positions.
/// </summary>
public decimal ExitStochasticLow
{
get => _exitStochLow.Value;
set => _exitStochLow.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public AltariusRsiStochasticDualStrategy()
{
_baseVolume = Param(nameof(BaseVolume), 1m)
.SetNotNegative()
.SetDisplay("Base Volume", "Initial volume before money management rules", "Trading")
.SetOptimize(0.1m, 2m, 0.1m);
_maximumRiskPercent = Param(nameof(MaximumRiskPercent), 0.1m)
.SetNotNegative()
.SetDisplay("Max Risk %", "Equity drawdown percentage that forces position closure", "Risk Management")
.SetOptimize(0.05m, 0.3m, 0.05m);
_decreaseFactor = Param(nameof(DecreaseFactor), 3m)
.SetNotNegative()
.SetDisplay("Decrease Factor", "Loss streak divider applied to volume", "Risk Management")
.SetOptimize(1m, 5m, 1m);
_rsiPeriod = Param(nameof(RsiPeriod), 4)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "Length of RSI used for exits", "Indicators")
.SetOptimize(2, 8, 1);
_slowStochPeriod = Param(nameof(SlowStochasticPeriod), 15)
.SetGreaterThanZero()
.SetDisplay("Slow Stochastic Period", "Main period of slow stochastic", "Indicators")
.SetOptimize(10, 25, 1);
_slowStochK = Param(nameof(SlowStochasticK), 8)
.SetGreaterThanZero()
.SetDisplay("Slow Stochastic %K", "Smoothing of %K for slow stochastic", "Indicators");
_slowStochD = Param(nameof(SlowStochasticD), 8)
.SetGreaterThanZero()
.SetDisplay("Slow Stochastic %D", "Smoothing of %D for slow stochastic", "Indicators");
_fastStochPeriod = Param(nameof(FastStochasticPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Fast Stochastic Period", "Main period of fast stochastic", "Indicators")
.SetOptimize(5, 15, 1);
_fastStochK = Param(nameof(FastStochasticK), 3)
.SetGreaterThanZero()
.SetDisplay("Fast Stochastic %K", "Smoothing of %K for fast stochastic", "Indicators");
_fastStochD = Param(nameof(FastStochasticD), 3)
.SetGreaterThanZero()
.SetDisplay("Fast Stochastic %D", "Smoothing of %D for fast stochastic", "Indicators");
_differenceThreshold = Param(nameof(StochasticDifferenceThreshold), 5m)
.SetNotNegative()
.SetDisplay("Momentum Threshold", "Minimum difference between fast stochastic lines", "Trading")
.SetOptimize(2m, 10m, 1m);
_buyLimit = Param(nameof(BuyStochasticLimit), 50m)
.SetDisplay("Buy Stochastic Limit", "Upper bound of slow stochastic for longs", "Trading");
_sellLimit = Param(nameof(SellStochasticLimit), 55m)
.SetDisplay("Sell Stochastic Limit", "Lower bound of slow stochastic for shorts", "Trading");
_exitRsiHigh = Param(nameof(ExitRsiHigh), 60m)
.SetDisplay("Exit RSI High", "RSI threshold to exit longs", "Exits");
_exitRsiLow = Param(nameof(ExitRsiLow), 40m)
.SetDisplay("Exit RSI Low", "RSI threshold to exit shorts", "Exits");
_exitStochHigh = Param(nameof(ExitStochasticHigh), 70m)
.SetDisplay("Exit Stochastic High", "Slow stochastic signal level confirming long exit", "Exits");
_exitStochLow = Param(nameof(ExitStochasticLow), 30m)
.SetDisplay("Exit Stochastic Low", "Slow stochastic signal level confirming short exit", "Exits");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candles used for calculations", "Market Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousSlowSignal = 0m;
_hasPreviousSlowSignal = false;
_lastRealizedPnL = PnLManager?.RealizedPnL ?? 0m;
_consecutiveLosses = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_lastRealizedPnL = PnLManager?.RealizedPnL ?? 0m;
var rsi = new RelativeStrengthIndex
{
Length = RsiPeriod,
};
var slowStochastic = new StochasticOscillator();
slowStochastic.K.Length = SlowStochasticK;
slowStochastic.D.Length = SlowStochasticD;
var fastStochastic = new StochasticOscillator();
fastStochastic.K.Length = FastStochasticK;
fastStochastic.D.Length = FastStochasticD;
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(rsi, slowStochastic, fastStochastic, ProcessIndicators)
.Start();
var chartArea = CreateChartArea();
if (chartArea != null)
{
DrawCandles(chartArea, subscription);
DrawIndicator(chartArea, rsi);
DrawIndicator(chartArea, slowStochastic);
DrawIndicator(chartArea, fastStochastic);
DrawOwnTrades(chartArea);
}
}
private void ProcessIndicators(ICandleMessage candle, IIndicatorValue rsiValue, IIndicatorValue slowValue, IIndicatorValue fastValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (MaximumRiskPercent > 0m)
{
var unrealizedPnL = PnLManager?.UnrealizedPnL ?? 0m;
if (unrealizedPnL < 0m)
{
var portfolio = Portfolio;
var equity = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
if (equity > 0m)
{
var allowedLoss = equity * MaximumRiskPercent;
if (Math.Abs(unrealizedPnL) >= allowedLoss)
{
CloseCurrentPosition();
return;
}
}
}
}
if (rsiValue.IsEmpty || slowValue.IsEmpty || fastValue.IsEmpty)
return;
var rsi = rsiValue.ToDecimal();
var slow = slowValue as StochasticOscillatorValue;
var fast = fastValue as StochasticOscillatorValue;
if (slow == null || fast == null)
return;
if (slow.K is not decimal slowMainValue ||
slow.D is not decimal slowSignalValue ||
fast.K is not decimal fastMainValue ||
fast.D is not decimal fastSignalValue)
{
return;
}
if (!_hasPreviousSlowSignal)
{
_previousSlowSignal = slowSignalValue;
_hasPreviousSlowSignal = true;
return;
}
if (Position == 0m)
{
var momentum = Math.Abs(fastMainValue - fastSignalValue);
if (slowMainValue > slowSignalValue && slowMainValue < BuyStochasticLimit && momentum > StochasticDifferenceThreshold)
{
EnterPosition(Sides.Buy);
}
else if (slowMainValue < slowSignalValue && slowMainValue > SellStochasticLimit && momentum > StochasticDifferenceThreshold)
{
EnterPosition(Sides.Sell);
}
}
else if (Position > 0m)
{
if (rsi > ExitRsiHigh && slowSignalValue < _previousSlowSignal && slowSignalValue > ExitStochasticHigh)
{
ExitPosition(Sides.Buy);
}
}
else if (Position < 0m)
{
if (rsi < ExitRsiLow && slowSignalValue > _previousSlowSignal && slowSignalValue < ExitStochasticLow)
{
ExitPosition(Sides.Sell);
}
}
_previousSlowSignal = slowSignalValue;
}
private void EnterPosition(Sides side)
{
var volume = CalculateOrderVolume();
if (volume <= 0m)
return;
if (side == Sides.Buy)
{
BuyMarket(volume);
}
else
{
SellMarket(volume);
}
}
private void ExitPosition(Sides side)
{
var position = Position;
if (position == 0m)
return;
if (side == Sides.Buy && position > 0m)
{
SellMarket(position);
}
else if (side == Sides.Sell && position < 0m)
{
BuyMarket(Math.Abs(position));
}
}
private void CloseCurrentPosition()
{
var position = Position;
if (position > 0m)
{
SellMarket(position);
}
else if (position < 0m)
{
BuyMarket(Math.Abs(position));
}
}
private decimal CalculateOrderVolume()
{
var volume = BaseVolume;
if (DecreaseFactor > 0m && _consecutiveLosses > 1)
{
var reduction = volume * _consecutiveLosses / DecreaseFactor;
volume -= reduction;
}
if (volume <= 0m)
volume = BaseVolume;
var security = Security;
if (security != null)
{
var step = security.VolumeStep ?? 0m;
if (step <= 0m)
step = 0.1m;
var minVolume = security.MinVolume ?? step;
var maxVolume = security.MaxVolume;
var steps = decimal.Floor(volume / step);
if (steps < 1m)
steps = 1m;
volume = steps * step;
if (volume < minVolume)
volume = minVolume;
if (maxVolume is decimal max && max > 0m && volume > max)
volume = max;
}
return volume;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (Position == 0m)
{
var realizedPnL = PnLManager?.RealizedPnL ?? 0m;
var gain = realizedPnL - _lastRealizedPnL;
_lastRealizedPnL = realizedPnL;
if (gain > 0m)
{
_consecutiveLosses = 0;
}
else if (gain < 0m)
{
_consecutiveLosses++;
}
}
}
}
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, Sides
from StockSharp.Algo.Indicators import RelativeStrengthIndex, StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
class altarius_rsi_stochastic_dual_strategy(Strategy):
"""
Strategy converted from AltariusRSIxampnSTOH MQL4 expert advisor.
Combines dual stochastic filters with RSI based exits and dynamic position sizing.
"""
def __init__(self):
super(altarius_rsi_stochastic_dual_strategy, self).__init__()
self._base_volume = self.Param("BaseVolume", 1.0) \
.SetDisplay("Base Volume", "Initial volume before money management rules", "Trading")
self._rsi_period = self.Param("RsiPeriod", 4) \
.SetGreaterThanZero() \
.SetDisplay("RSI Period", "Length of RSI used for exits", "Indicators")
self._slow_stoch_k = self.Param("SlowStochasticK", 8) \
.SetGreaterThanZero() \
.SetDisplay("Slow Stochastic %K", "Smoothing of %K for slow stochastic", "Indicators")
self._slow_stoch_d = self.Param("SlowStochasticD", 8) \
.SetGreaterThanZero() \
.SetDisplay("Slow Stochastic %D", "Smoothing of %D for slow stochastic", "Indicators")
self._fast_stoch_k = self.Param("FastStochasticK", 3) \
.SetGreaterThanZero() \
.SetDisplay("Fast Stochastic %K", "Smoothing of %K for fast stochastic", "Indicators")
self._fast_stoch_d = self.Param("FastStochasticD", 3) \
.SetGreaterThanZero() \
.SetDisplay("Fast Stochastic %D", "Smoothing of %D for fast stochastic", "Indicators")
self._diff_threshold = self.Param("StochasticDifferenceThreshold", 5.0) \
.SetDisplay("Momentum Threshold", "Minimum difference between fast stochastic lines", "Trading")
self._buy_limit = self.Param("BuyStochasticLimit", 50.0) \
.SetDisplay("Buy Stochastic Limit", "Upper bound of slow stochastic for longs", "Trading")
self._sell_limit = self.Param("SellStochasticLimit", 55.0) \
.SetDisplay("Sell Stochastic Limit", "Lower bound of slow stochastic for shorts", "Trading")
self._exit_rsi_high = self.Param("ExitRsiHigh", 60.0) \
.SetDisplay("Exit RSI High", "RSI threshold to exit longs", "Exits")
self._exit_rsi_low = self.Param("ExitRsiLow", 40.0) \
.SetDisplay("Exit RSI Low", "RSI threshold to exit shorts", "Exits")
self._exit_stoch_high = self.Param("ExitStochasticHigh", 70.0) \
.SetDisplay("Exit Stochastic High", "Slow stochastic signal level confirming long exit", "Exits")
self._exit_stoch_low = self.Param("ExitStochasticLow", 30.0) \
.SetDisplay("Exit Stochastic Low", "Slow stochastic signal level confirming short exit", "Exits")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candles used for calculations", "Market Data")
self._prev_slow_signal = 0.0
self._has_prev_slow_signal = False
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, v): self._candle_type.Value = v
def OnReseted(self):
super(altarius_rsi_stochastic_dual_strategy, self).OnReseted()
self._prev_slow_signal = 0.0
self._has_prev_slow_signal = False
def OnStarted2(self, time):
super(altarius_rsi_stochastic_dual_strategy, self).OnStarted2(time)
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
slow_stoch = StochasticOscillator()
slow_stoch.K.Length = self._slow_stoch_k.Value
slow_stoch.D.Length = self._slow_stoch_d.Value
fast_stoch = StochasticOscillator()
fast_stoch.K.Length = self._fast_stoch_k.Value
fast_stoch.D.Length = self._fast_stoch_d.Value
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(rsi, slow_stoch, fast_stoch, self.ProcessIndicators).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, rsi)
self.DrawOwnTrades(area)
def ProcessIndicators(self, candle, rsi_value, slow_value, fast_value):
if candle.State != CandleStates.Finished:
return
if rsi_value.IsEmpty or slow_value.IsEmpty or fast_value.IsEmpty:
return
rsi = float(rsi_value)
slow_k_raw = slow_value.K if hasattr(slow_value, 'K') else None
slow_d_raw = slow_value.D if hasattr(slow_value, 'D') else None
fast_k_raw = fast_value.K if hasattr(fast_value, 'K') else None
fast_d_raw = fast_value.D if hasattr(fast_value, 'D') else None
if slow_k_raw is None or slow_d_raw is None or fast_k_raw is None or fast_d_raw is None:
return
slow_k = float(slow_k_raw)
slow_d = float(slow_d_raw)
fast_k = float(fast_k_raw)
fast_d = float(fast_d_raw)
if not self._has_prev_slow_signal:
self._prev_slow_signal = slow_d
self._has_prev_slow_signal = True
return
if self.Position == 0:
momentum = abs(fast_k - fast_d)
if slow_k > slow_d and slow_k < self._buy_limit.Value and momentum > self._diff_threshold.Value:
self.BuyMarket(self.Volume)
elif slow_k < slow_d and slow_k > self._sell_limit.Value and momentum > self._diff_threshold.Value:
self.SellMarket(self.Volume)
elif self.Position > 0:
if rsi > self._exit_rsi_high.Value and slow_d < self._prev_slow_signal and slow_d > self._exit_stoch_high.Value:
self.SellMarket(self.Position)
elif self.Position < 0:
if rsi < self._exit_rsi_low.Value and slow_d > self._prev_slow_signal and slow_d < self._exit_stoch_low.Value:
self.BuyMarket(Math.Abs(self.Position))
self._prev_slow_signal = slow_d
def CreateClone(self):
"""!! REQUIRED!! Creates a new instance of the strategy."""
return altarius_rsi_stochastic_dual_strategy()