The Scalpel Strategy is a StockSharp port of the MetaTrader 4 expert advisor Scalpel.mq4. The system searches for momentum breakouts on the base timeframe, confirms the move with higher-timeframe lows/highs, and filters the entries using a directional volume study built on 1-minute candles. Position management mirrors the original EA: profits are harvested with a fixed take-profit that shrinks over time, the stop-loss can trail once price has moved in favour of the trade, and every position can be force-closed after a configurable lifetime or on Friday evening.
Trading Logic
Multi-timeframe trend filter: a long signal requires that the current lows on H4, H1, and M30 candles are higher than their previous lows. Short signals demand lower highs on the same timeframes.
Breakout confirmation: the strategy waits for the best ask to exceed the previous high (long) or the best bid to drop below the previous low (short) on the base timeframe. Additionally the previous three highs (or lows) must form a staircase in the breakout direction.
CCI window: the Commodity Channel Index from the previous closed candle must stay within a configurable band around zero. Positive limits use a symmetric window; negative limits relax the requirement for one of the sides exactly like in the original EA.
Directional volume filter: volumes from the volatility timeframe are split into two rolling blocks. A trade is allowed only if the most recent block shows more directional volume than the older block and the older block is non-zero. Negative VolatilityWindow values switch the filter to range-based (non-directional) accumulation.
Risk management:
Fixed take-profit and stop-loss distances expressed in minimum price steps.
The take-profit level is reduced by one price step every TakeProfitReduceMinutes minutes that the position stays open.
A trailing stop activates after price has moved by TrailingStopPoints and then follows the move candle by candle.
Positions can be forcibly closed after LiveMinutes or at the configured FridayCloseHour.
New entries are blocked while the absolute net position equals MaxDirectionalPositions * TradeVolume and optionally while the re-entry cooldown is active.
Parameters
Name
Default
Description
TradeVolume
-5
Order size. Positive values use fixed lots; negative values represent a percentage of portfolio capital converted to volume using the current ask price.
TakeProfitPoints
40
Distance from entry to the take-profit target in price steps.
StopLossPoints
340
Distance from entry to the stop-loss in price steps.
TrailingStopPoints
25
Trailing stop distance in price steps. The trail engages once the move exceeds this distance.
CciPeriod
14
Lookback period for the Commodity Channel Index calculated on the base timeframe.
CciLimit
75
Upper bound for long entries and mirrored negative bound for short entries. Negative values reproduce the asymmetric limits from the original EA.
MaxDirectionalPositions
1
Maximum net position units (in multiples of the calculated trade volume) allowed in one direction.
ReentryIntervalMinutes
0
Minimum number of minutes to wait between two consecutive entries.
TakeProfitReduceMinutes
600
Minutes before the take-profit threshold is reduced by one price step. Set to 0 to disable the reduction.
LiveMinutes
0
Maximum lifetime of a position in minutes. A value of 0 disables the timer.
VolatilityWindow
100
Number of volatility candles stored in each rolling block. Negative values switch to range-based accumulation, 0 uses only the latest candle.
VolatilityThresholdPoints
1
Minimum candle body (positive window) or range (non-directional window) required to accumulate volume. The sign flips the interpretation of up/down volumes.
FridayCloseHour
22
Hour of day (0-23) used to liquidate positions on Friday evenings. 0 disables the Friday exit.
SpreadLimitPoints
5.5
Maximum allowed spread in price steps when opening a new position.
CandleType
1 minute
Base timeframe that generates entries and manages exits.
Hour1CandleType
1 hour
Higher timeframe used for H1 trend confirmation.
Hour4CandleType
4 hours
Higher timeframe used for H4 trend confirmation.
Minute30CandleType
30 minutes
Higher timeframe used for M30 trend confirmation.
VolatilityCandleType
1 minute
Timeframe that feeds the directional volume filter.
Implementation Notes
The strategy subscribes to the order book to reuse the latest best bid/ask prices for breakout detection and spread filtering.
All indicator bindings rely on StockSharp's high-level API: the CCI value is obtained through BindEx, while higher timeframes use dedicated subscriptions.
Trailing stops and take-profit reductions are executed in code rather than via protective orders to mimic the original EA behaviour.
Negative TradeVolume values rely on the current ask price and security volume constraints. When the calculated size falls below the minimum lot, it is automatically rounded up.
Usage
Attach the strategy to a portfolio and choose the desired security.
Configure the timeframe parameters, risk thresholds, and volume sizing rules.
Start the strategy. Signals are evaluated on finished candles only; positions are opened with market orders and closed via the built-in risk management rules.
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Scalpel: CCI-based scalping with EMA filter and ATR stops.
/// </summary>
public class ScalpelStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<int> _atrLength;
private decimal _prevRsi;
private decimal _entryPrice;
public ScalpelStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_rsiLength = Param(nameof(RsiLength), 14)
.SetDisplay("RSI Length", "RSI period.", "Indicators");
_emaLength = Param(nameof(EmaLength), 20)
.SetDisplay("EMA Length", "Trend filter.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
public int EmaLength
{
get => _emaLength.Value;
set => _emaLength.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevRsi = 0;
_entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevRsi = 0;
_entryPrice = 0;
var rsi = new RelativeStrengthIndex { Length = RsiLength };
var ema = new ExponentialMovingAverage { Length = EmaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(rsi, ema, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal rsiVal, decimal emaVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
if (_prevRsi == 0 || atrVal <= 0)
{
_prevRsi = rsiVal;
return;
}
var close = candle.ClosePrice;
if (Position > 0)
{
if (close >= _entryPrice + atrVal * 2m || close <= _entryPrice - atrVal * 1.5m || rsiVal > 70)
{
SellMarket();
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (close <= _entryPrice - atrVal * 2m || close >= _entryPrice + atrVal * 1.5m || rsiVal < 30)
{
BuyMarket();
_entryPrice = 0;
}
}
if (Position == 0)
{
if (rsiVal > 50 && _prevRsi <= 50 && close > emaVal)
{
_entryPrice = close;
BuyMarket();
}
else if (rsiVal < 50 && _prevRsi >= 50 && close < emaVal)
{
_entryPrice = close;
SellMarket();
}
}
_prevRsi = rsiVal;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import RelativeStrengthIndex, ExponentialMovingAverage, AverageTrueRange
class scalpel_strategy(Strategy):
def __init__(self):
super(scalpel_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(8))) \
.SetDisplay("Candle Type", "Timeframe.", "General")
self._rsi_length = self.Param("RsiLength", 14) \
.SetDisplay("RSI Length", "RSI period.", "Indicators")
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Length", "Trend filter.", "Indicators")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period.", "Indicators")
self._prev_rsi = 0.0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def RsiLength(self):
return self._rsi_length.Value
@property
def EmaLength(self):
return self._ema_length.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(scalpel_strategy, self).OnStarted2(time)
self._prev_rsi = 0.0
self._entry_price = 0.0
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiLength
self._ema = ExponentialMovingAverage()
self._ema.Length = self.EmaLength
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._rsi, self._ema, self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, rsi_val, ema_val, atr_val):
if candle.State != CandleStates.Finished:
return
rv = float(rsi_val)
ev = float(ema_val)
av = float(atr_val)
if self._prev_rsi == 0 or av <= 0:
self._prev_rsi = rv
return
close = float(candle.ClosePrice)
if self.Position > 0:
if close >= self._entry_price + av * 2.0 or close <= self._entry_price - av * 1.5 or rv > 70:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if close <= self._entry_price - av * 2.0 or close >= self._entry_price + av * 1.5 or rv < 30:
self.BuyMarket()
self._entry_price = 0.0
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_rsi = rv
return
if self.Position == 0:
if rv > 50 and self._prev_rsi <= 50 and close > ev:
self._entry_price = close
self.BuyMarket()
elif rv < 50 and self._prev_rsi >= 50 and close < ev:
self._entry_price = close
self.SellMarket()
self._prev_rsi = rv
def OnReseted(self):
super(scalpel_strategy, self).OnReseted()
self._prev_rsi = 0.0
self._entry_price = 0.0
def CreateClone(self):
return scalpel_strategy()