Hybrid Scalping Bot Strategy
A hybrid scalping system combining RSI signals with EMA trend filters and optional volume confirmation. The bot can adjust signal sensitivity from very easy to strong and includes quick exit and trailing stop features.
Testing indicates an average annual return of about 35%. It works best on liquid crypto pairs.
The strategy enters long or short based on RSI thresholds and candle strength, optionally filtered by trend and volume. Positions are protected with configurable take-profit, stop-loss and trailing logic, and daily trade limits reset at the start of each session.
Details
- Entry Criteria:
- Buy: RSI below 30 with bullish candle, optional trend/volume filters depending on sensitivity.
- Sell: RSI above 70 with bearish candle, optional trend/volume filters.
- Long/Short: Both.
- Exit Criteria:
- Take profit, stop loss, trailing stop or quick RSI/EMA reversal.
- Stops: Yes, percentage-based SL/TP and optional trailing stop.
- Filters:
- Trend and volume filters depending on configuration.
using System;
using System.Linq;
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>
/// RSI based hybrid scalping strategy with adjustable sensitivity.
/// Combines RSI, EMA trend filters and optional volume filter.
/// </summary>
public class HybridScalpingBotStrategy : Strategy
{
private readonly StrategyParam<int> _dailyTradeLimit;
private readonly StrategyParam<decimal> _takeProfitPercent;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<bool> _useQuickExit;
private readonly StrategyParam<bool> _useTrailingStop;
private readonly StrategyParam<decimal> _trailingStopPercent;
private readonly StrategyParam<string> _signalSensitivity;
private readonly StrategyParam<bool> _useTrendFilter;
private readonly StrategyParam<bool> _useVolumeFilter;
private readonly StrategyParam<DataType> _candleType;
private decimal _entryPrice;
private decimal _highestPrice;
private decimal _lowestPrice;
private int _tradesToday;
private DateTime _lastDate;
/// <summary>
/// Maximum trades per day.
/// </summary>
public int DailyTradeLimit { get => _dailyTradeLimit.Value; set => _dailyTradeLimit.Value = value; }
/// <summary>
/// Take profit percentage.
/// </summary>
public decimal TakeProfitPercent { get => _takeProfitPercent.Value; set => _takeProfitPercent.Value = value; }
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
/// <summary>
/// Exit early on RSI or EMA reversal.
/// </summary>
public bool UseQuickExit { get => _useQuickExit.Value; set => _useQuickExit.Value = value; }
/// <summary>
/// Enable trailing stop.
/// </summary>
public bool UseTrailingStop { get => _useTrailingStop.Value; set => _useTrailingStop.Value = value; }
/// <summary>
/// Trailing stop percent.
/// </summary>
public decimal TrailingStopPercent { get => _trailingStopPercent.Value; set => _trailingStopPercent.Value = value; }
/// <summary>
/// Signal sensitivity level.
/// </summary>
public string SignalSensitivity { get => _signalSensitivity.Value; set => _signalSensitivity.Value = value; }
/// <summary>
/// Use EMA trend filter.
/// </summary>
public bool UseTrendFilter { get => _useTrendFilter.Value; set => _useTrendFilter.Value = value; }
/// <summary>
/// Require high volume.
/// </summary>
public bool UseVolumeFilter { get => _useVolumeFilter.Value; set => _useVolumeFilter.Value = value; }
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>
/// Initializes a new instance of the <see cref="HybridScalpingBotStrategy"/> class.
/// </summary>
public HybridScalpingBotStrategy()
{
_dailyTradeLimit = Param(nameof(DailyTradeLimit), 15)
.SetGreaterThanZero()
.SetDisplay("Daily Trades", "Maximum trades per day", "General");
_takeProfitPercent = Param(nameof(TakeProfitPercent), 0.8m)
.SetGreaterThanZero()
.SetDisplay("Take Profit %", "Take profit percentage", "Risk")
.SetOptimize(0.5m, 3m, 0.5m);
_stopLossPercent = Param(nameof(StopLossPercent), 0.6m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
.SetOptimize(0.5m, 2m, 0.5m);
_useQuickExit = Param(nameof(UseQuickExit), true)
.SetDisplay("Use Quick Exit", "Exit on RSI or EMA pullback", "General");
_useTrailingStop = Param(nameof(UseTrailingStop), true)
.SetDisplay("Use Trailing Stop", "Trail profit after entry", "General");
_trailingStopPercent = Param(nameof(TrailingStopPercent), 0.4m)
.SetGreaterThanZero()
.SetDisplay("Trailing Stop %", "Trailing stop percent", "Risk")
.SetOptimize(0.2m, 1m, 0.2m);
_signalSensitivity = Param(nameof(SignalSensitivity), "Easy")
.SetDisplay("Signal Level", "VeryEasy / Easy / Medium / Strong", "General");
_useTrendFilter = Param(nameof(UseTrendFilter), true)
.SetDisplay("Use Trend Filter", "Trade only with trend", "General");
_useVolumeFilter = Param(nameof(UseVolumeFilter), false)
.SetDisplay("Use Volume Filter", "Require high volume", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_highestPrice = 0;
_lowestPrice = 0;
_tradesToday = 0;
_lastDate = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var rsi = new RelativeStrengthIndex { Length = 14 };
var ema9 = new ExponentialMovingAverage { Length = 9 };
var ema21 = new ExponentialMovingAverage { Length = 21 };
var ema50 = new ExponentialMovingAverage { Length = 50 };
var volumeSma = new SimpleMovingAverage { Length = 10 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(rsi, ema9, ema21, ema50, volumeSma, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal rsi, decimal ema9, decimal ema21, decimal ema50, decimal avgVolume)
{
if (candle.State != CandleStates.Finished)
return;
if (candle.OpenTime.Date != _lastDate)
{
_tradesToday = 0;
_lastDate = candle.OpenTime.Date;
}
var bullish = candle.ClosePrice > candle.OpenPrice;
var bearish = candle.ClosePrice < candle.OpenPrice;
var bodyRatio = candle.HighPrice == candle.LowPrice ? 0m : (candle.ClosePrice - candle.OpenPrice) / (candle.HighPrice - candle.LowPrice);
var strongBullish = bullish && bodyRatio > 0.6m;
var strongBearish = bearish && bodyRatio > 0.6m;
var uptrend = ema21 > ema50;
var downtrend = ema21 < ema50;
var strongUptrend = ema9 > ema21 && ema21 > ema50;
var strongDowntrend = ema9 < ema21 && ema21 < ema50;
var volumeOk = !UseVolumeFilter || candle.TotalVolume > avgVolume * 1.2m;
bool buySignal;
bool sellSignal;
switch (SignalSensitivity)
{
case "VeryEasy":
buySignal = rsi < 60 && bullish;
sellSignal = rsi > 40 && bearish;
break;
case "Medium":
buySignal = rsi < 30 && bullish && (!UseTrendFilter || uptrend);
sellSignal = rsi > 70 && bearish && (!UseTrendFilter || downtrend);
break;
case "Strong":
buySignal = rsi < 30 && strongBullish && (!UseTrendFilter || strongUptrend) && volumeOk && candle.ClosePrice > ema21;
sellSignal = rsi > 70 && strongBearish && (!UseTrendFilter || strongDowntrend) && volumeOk && candle.ClosePrice < ema21;
break;
default:
buySignal = rsi < 30 && bullish;
sellSignal = rsi > 70 && bearish;
break;
}
var canTrade = _tradesToday < DailyTradeLimit && Position == 0;
if (buySignal && canTrade)
{
BuyMarket();
_tradesToday++;
_entryPrice = candle.ClosePrice;
_highestPrice = candle.ClosePrice;
}
else if (sellSignal && canTrade)
{
SellMarket();
_tradesToday++;
_entryPrice = candle.ClosePrice;
_lowestPrice = candle.ClosePrice;
}
if (Position > 0)
{
_highestPrice = Math.Max(_highestPrice, candle.HighPrice);
if (UseTrailingStop)
{
var trail = _highestPrice * (1 - TrailingStopPercent / 100m);
if (candle.ClosePrice <= trail)
SellMarket();
}
if (candle.ClosePrice <= _entryPrice * (1 - StopLossPercent / 100m))
SellMarket();
else if (candle.ClosePrice >= _entryPrice * (1 + TakeProfitPercent / 100m))
SellMarket();
else if (UseQuickExit && (rsi > 70 || rsi < 25 || candle.ClosePrice < ema21))
SellMarket();
}
else if (Position < 0)
{
_lowestPrice = Math.Min(_lowestPrice, candle.LowPrice);
if (UseTrailingStop)
{
var trail = _lowestPrice * (1 + TrailingStopPercent / 100m);
if (candle.ClosePrice >= trail)
BuyMarket();
}
if (candle.ClosePrice >= _entryPrice * (1 + StopLossPercent / 100m))
BuyMarket();
else if (candle.ClosePrice <= _entryPrice * (1 - TakeProfitPercent / 100m))
BuyMarket();
else if (UseQuickExit && (rsi < 30 || rsi > 75 || candle.ClosePrice > ema21))
BuyMarket();
}
}
}
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 ExponentialMovingAverage, RelativeStrengthIndex, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class hybrid_scalping_bot_strategy(Strategy):
def __init__(self):
super(hybrid_scalping_bot_strategy, self).__init__()
self._daily_trade_limit = self.Param("DailyTradeLimit", 15) \
.SetDisplay("Daily Trades", "Maximum trades per day", "General")
self._take_profit_percent = self.Param("TakeProfitPercent", 0.8) \
.SetDisplay("Take Profit %", "Take profit percentage", "Risk")
self._stop_loss_percent = self.Param("StopLossPercent", 0.6) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
self._use_quick_exit = self.Param("UseQuickExit", True) \
.SetDisplay("Use Quick Exit", "Exit on RSI or EMA pullback", "General")
self._use_trailing_stop = self.Param("UseTrailingStop", True) \
.SetDisplay("Use Trailing Stop", "Trail profit after entry", "General")
self._trailing_stop_percent = self.Param("TrailingStopPercent", 0.4) \
.SetDisplay("Trailing Stop %", "Trailing stop percent", "Risk")
self._signal_sensitivity = self.Param("SignalSensitivity", "Easy") \
.SetDisplay("Signal Level", "VeryEasy / Easy / Medium / Strong", "General")
self._use_trend_filter = self.Param("UseTrendFilter", True) \
.SetDisplay("Use Trend Filter", "Trade only with trend", "General")
self._use_volume_filter = self.Param("UseVolumeFilter", False) \
.SetDisplay("Use Volume Filter", "Require high volume", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._entry_price = 0.0
self._highest_price = 0.0
self._lowest_price = 0.0
self._trades_today = 0
self._last_date = None
@property
def daily_trade_limit(self):
return self._daily_trade_limit.Value
@property
def take_profit_percent(self):
return self._take_profit_percent.Value
@property
def stop_loss_percent(self):
return self._stop_loss_percent.Value
@property
def use_quick_exit(self):
return self._use_quick_exit.Value
@property
def use_trailing_stop(self):
return self._use_trailing_stop.Value
@property
def trailing_stop_percent(self):
return self._trailing_stop_percent.Value
@property
def signal_sensitivity(self):
return self._signal_sensitivity.Value
@property
def use_trend_filter(self):
return self._use_trend_filter.Value
@property
def use_volume_filter(self):
return self._use_volume_filter.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(hybrid_scalping_bot_strategy, self).OnReseted()
self._entry_price = 0.0
self._highest_price = 0.0
self._lowest_price = 0.0
self._trades_today = 0
self._last_date = None
def OnStarted2(self, time):
super(hybrid_scalping_bot_strategy, self).OnStarted2(time)
rsi = RelativeStrengthIndex()
rsi.Length = 14
ema9 = ExponentialMovingAverage()
ema9.Length = 9
ema21 = ExponentialMovingAverage()
ema21.Length = 21
ema50 = ExponentialMovingAverage()
ema50.Length = 50
volume_sma = SimpleMovingAverage()
volume_sma.Length = 10
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(rsi, ema9, ema21, ema50, volume_sma, self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def on_process(self, candle, rsi, ema9, ema21, ema50, avg_volume):
if candle.State != CandleStates.Finished:
return
if candle.OpenTime.Date != self._last_date:
self._trades_today = 0
self._last_date = candle.OpenTime.Date
bullish = candle.ClosePrice > candle.OpenPrice
bearish = candle.ClosePrice < candle.OpenPrice
body_ratio = (0 if candle.HighPrice == candle.LowPrice else (candle.ClosePrice - candle.OpenPrice) / (candle.HighPrice - candle.LowPrice))
strong_bullish = bullish and body_ratio > 0.6
strong_bearish = bearish and body_ratio > 0.6
uptrend = ema21 > ema50
downtrend = ema21 < ema50
strong_uptrend = ema9 > ema21 and ema21 > ema50
strong_downtrend = ema9 < ema21 and ema21 < ema50
volume_ok = not self.use_volume_filter or candle.TotalVolume > avg_volume * 1.2
sensitivity = self.signal_sensitivity
if sensitivity == "VeryEasy":
buy_signal = rsi < 60 and bullish
sell_signal = rsi > 40 and bearish
elif sensitivity == "Medium":
buy_signal = rsi < 30 and bullish and (not self.use_trend_filter or uptrend)
sell_signal = rsi > 70 and bearish and (not self.use_trend_filter or downtrend)
elif sensitivity == "Strong":
buy_signal = rsi < 30 and strong_bullish and (not self.use_trend_filter or strong_uptrend) and volume_ok and candle.ClosePrice > ema21
sell_signal = rsi > 70 and strong_bearish and (not self.use_trend_filter or strong_downtrend) and volume_ok and candle.ClosePrice < ema21
else:
buy_signal = rsi < 30 and bullish
sell_signal = rsi > 70 and bearish
can_trade = self._trades_today < self.daily_trade_limit and self.Position == 0
if buy_signal and can_trade:
self.BuyMarket()
self._trades_today += 1
self._entry_price = candle.ClosePrice
self._highest_price = candle.ClosePrice
elif sell_signal and can_trade:
self.SellMarket()
self._trades_today += 1
self._entry_price = candle.ClosePrice
self._lowest_price = candle.ClosePrice
if self.Position > 0:
self._highest_price = max(self._highest_price, candle.HighPrice)
if self.use_trailing_stop:
trail = self._highest_price * (1 - self.trailing_stop_percent / 100)
if candle.ClosePrice <= trail:
self.SellMarket()
if candle.ClosePrice <= self._entry_price * (1 - self.stop_loss_percent / 100):
self.SellMarket()
elif candle.ClosePrice >= self._entry_price * (1 + self.take_profit_percent / 100):
self.SellMarket()
elif self.use_quick_exit and (rsi > 70 or rsi < 25 or candle.ClosePrice < ema21):
self.SellMarket()
elif self.Position < 0:
self._lowest_price = min(self._lowest_price, candle.LowPrice)
if self.use_trailing_stop:
trail = self._lowest_price * (1 + self.trailing_stop_percent / 100)
if candle.ClosePrice >= trail:
self.BuyMarket()
if candle.ClosePrice >= self._entry_price * (1 + self.stop_loss_percent / 100):
self.BuyMarket()
elif candle.ClosePrice <= self._entry_price * (1 - self.take_profit_percent / 100):
self.BuyMarket()
elif self.use_quick_exit and (rsi < 30 or rsi > 75 or candle.ClosePrice > ema21):
self.BuyMarket()
def CreateClone(self):
return hybrid_scalping_bot_strategy()