Tick-based trailing stop manager converted from the MetaTrader expert advisor e_Breakeven_v4.
Monitors every trade tick to move a virtual stop-loss once price travels far enough from the entry.
Closes long or short positions at market when the trailing level is hit, replicating the breakeven-plus-step behaviour of the original EA.
Includes an optional demo mode that randomly opens positions during testing to demonstrate the trailing logic without an external signal source.
How It Works
The strategy subscribes to trade ticks (DataType.Ticks) to emulate the OnTick callback used in MQL5.
When a position exists and the trailing stop (in pips) plus the trailing step have been exceeded, the stop level is shifted closer to price.
For long positions, the stop is placed at current price - trailing stop if the move from the entry exceeds trailing stop + trailing step.
For short positions, the stop is placed at current price + trailing stop when the price moves downward by the same distance.
If the live price touches or crosses the stored stop level, the strategy exits the entire position at market and resets the trailing state.
An internal pip conversion multiplies the broker price step by 10 when the instrument has 3 or 5 decimal digits, matching the MQL5 point-to-pip adjustment.
When demo mode is enabled, the strategy opens a random long or short trade (using the configured Volume) the first time a new tick arrives after the previous entry was closed.
Parameters
Name
Description
Default
Notes
TrailingStopPips
Distance in pips between the current price and the trailing stop.
10
Set to 0 to disable trailing completely.
TrailingStepPips
Additional pip distance required before the stop is advanced again.
1
Must be greater than zero when the trailing stop is active, reproducing the EA validation rule.
EnableDemoEntries
Enables random entries for backtests without an external signal.
false
When true, the strategy flips a coin on each tick while flat to decide the direction.
Position Management Rules
The strategy does not open positions by itself unless EnableDemoEntries is set to true.
Trailing is symmetric for long and short positions and works with any volume size.
Stop levels are managed internally (virtual) and enforced with market exits, avoiding explicit stop orders that may not be supported by every connector.
Any manual trade or external strategy can supply the entries; this component will only manage the trailing stop.
Usage Notes
Works best with instruments that provide trade ticks so the trailing reacts immediately.
Ensure Volume is configured to the lot size that matches the incoming positions if demo mode is used.
The pip conversion assumes FX-style pricing where symbols with 3 or 5 decimal places need a ×10 multiplier to turn points into pips.
The exit is triggered on the first tick that crosses the stored stop price, matching the immediate modification-and-close flow from the MQL logic.
Differences from the Original MQL5 Expert
Uses virtual stops with market exits instead of modifying broker-side stop-loss orders because StockSharp strategies typically manage exits through strategy logic.
Replaces the MetaTrader tester random entry block with the configurable EnableDemoEntries flag.
Converts the point-to-pip logic using Security.PriceStep and decimal counting instead of Symbol().Digits().
All comments and logging are now in English in accordance with repository guidelines.
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>
/// Trailing stop manager that moves stops to breakeven and beyond once price advances.
/// Designed to trail any manually opened position using pip based distances.
/// </summary>
public class BreakevenTrailingStopTickStrategy : Strategy
{
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<bool> _enableDemoEntries;
private readonly StrategyParam<DataType> _candleType;
private decimal _pointValue;
private decimal? _longStopPrice;
private decimal? _shortStopPrice;
private bool _exitOrderPending;
private decimal _entryPrice;
private DateTimeOffset? _lastDemoEntryTime;
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Trailing step in pips before the stop is moved again.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Enable random demo entries to showcase the trailing behaviour in testing.
/// </summary>
public bool EnableDemoEntries
{
get => _enableDemoEntries.Value;
set => _enableDemoEntries.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="BreakevenTrailingStopTickStrategy"/>.
/// </summary>
public BreakevenTrailingStopTickStrategy()
{
_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
.SetNotNegative()
.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Trailing")
.SetOptimize(5m, 30m, 5m);
_trailingStepPips = Param(nameof(TrailingStepPips), 1m)
.SetNotNegative()
.SetDisplay("Trailing Step", "Additional pips required before stop moves again", "Trailing")
.SetOptimize(0.5m, 5m, 0.5m);
_enableDemoEntries = Param(nameof(EnableDemoEntries), true)
.SetDisplay("Enable Demo Entries", "Automatically open random trades in testing", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for candles", "General");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pointValue = 0m;
_longStopPrice = null;
_shortStopPrice = null;
_exitOrderPending = false;
_lastDemoEntryTime = null;
_entryPrice = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");
_pointValue = CalculateAdjustedPoint();
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var price = candle.ClosePrice;
if (EnableDemoEntries)
TryCreateDemoEntry(candle, price);
if (Position == 0)
{
ResetTrailingState();
return;
}
if (TrailingStopPips <= 0m || _pointValue <= 0m)
return;
if (Position > 0)
UpdateLongTrailing(price);
else if (Position < 0)
UpdateShortTrailing(price);
}
private void TryCreateDemoEntry(ICandleMessage candle, decimal price)
{
if (Position != 0 || _exitOrderPending)
return;
var serverTime = candle.CloseTime;
if (_lastDemoEntryTime.HasValue && (serverTime - _lastDemoEntryTime.Value).TotalMinutes < 30)
return;
var volume = Volume;
if (volume <= 0m)
return;
if (Random.Shared.NextDouble() < 0.5)
{
BuyMarket(volume);
_entryPrice = price;
}
else
{
SellMarket(volume);
_entryPrice = price;
}
_lastDemoEntryTime = serverTime;
}
private void UpdateLongTrailing(decimal currentPrice)
{
var entryPrice = _entryPrice;
if (entryPrice <= 0m)
return;
var stopOffset = TrailingStopPips * _pointValue;
var stepOffset = TrailingStepPips * _pointValue;
if (stopOffset <= 0m)
return;
var activationOffset = stopOffset + stepOffset;
if (currentPrice - entryPrice <= activationOffset)
return;
var threshold = currentPrice - activationOffset;
if (!_longStopPrice.HasValue || _longStopPrice.Value < threshold)
{
var newStop = currentPrice - stopOffset;
if (newStop > 0m)
{
_longStopPrice = newStop;
// log($"Long trailing stop moved to {newStop}.");
}
}
if (_longStopPrice.HasValue && currentPrice <= _longStopPrice.Value)
ExitLongPosition();
}
private void UpdateShortTrailing(decimal currentPrice)
{
var entryPrice = _entryPrice;
if (entryPrice <= 0m)
return;
var stopOffset = TrailingStopPips * _pointValue;
var stepOffset = TrailingStepPips * _pointValue;
if (stopOffset <= 0m)
return;
var activationOffset = stopOffset + stepOffset;
if (entryPrice - currentPrice <= activationOffset)
return;
var threshold = currentPrice + activationOffset;
if (!_shortStopPrice.HasValue || _shortStopPrice.Value > threshold)
{
var newStop = currentPrice + stopOffset;
_shortStopPrice = newStop;
// log($"Short trailing stop moved to {newStop}.");
}
if (_shortStopPrice.HasValue && currentPrice >= _shortStopPrice.Value)
ExitShortPosition();
}
private void ExitLongPosition()
{
if (_exitOrderPending)
return;
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
SellMarket(volume);
_exitOrderPending = true;
// log("Long position closed by trailing stop.");
}
private void ExitShortPosition()
{
if (_exitOrderPending)
return;
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
BuyMarket(volume);
_exitOrderPending = true;
// log("Short position closed by trailing stop.");
}
private void ResetTrailingState()
{
_longStopPrice = null;
_shortStopPrice = null;
_exitOrderPending = false;
_entryPrice = 0m;
}
private decimal CalculateAdjustedPoint()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
var decimals = CountDecimals(step);
return decimals is 3 or 5 ? step * 10m : step;
}
private static int CountDecimals(decimal value)
{
value = Math.Abs(value);
var decimals = 0;
while (value != Math.Truncate(value) && decimals < 10)
{
value *= 10m;
decimals++;
}
return decimals;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class breakeven_trailing_stop_tick_strategy(Strategy):
def __init__(self):
super(breakeven_trailing_stop_tick_strategy, self).__init__()
self._trailing_stop_pips = self.Param("TrailingStopPips", 10.0)
self._trailing_step_pips = self.Param("TrailingStepPips", 1.0)
self._enable_demo_entries = self.Param("EnableDemoEntries", True)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._point_value = 0.0
self._long_stop_price = None
self._short_stop_price = None
self._exit_order_pending = False
self._entry_price = 0.0
self._last_demo_entry_time = None
self._candle_count = 0
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(breakeven_trailing_stop_tick_strategy, self).OnStarted2(time)
self._point_value = self._calculate_adjusted_point()
self._candle_count = 0
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
self._candle_count += 1
if self._enable_demo_entries.Value:
self._try_create_demo_entry(candle, price)
if self.Position == 0:
self._reset_trailing_state()
return
if self._trailing_stop_pips.Value <= 0 or self._point_value <= 0:
return
if self.Position > 0:
self._update_long_trailing(price)
elif self.Position < 0:
self._update_short_trailing(price)
def _try_create_demo_entry(self, candle, price):
if self.Position != 0 or self._exit_order_pending:
return
server_time = candle.CloseTime
if self._last_demo_entry_time is not None and (server_time - self._last_demo_entry_time).TotalMinutes < 30:
return
volume = float(self.Volume)
if volume <= 0:
return
# Use candle count parity as deterministic pseudo-random for demo entries
if self._candle_count % 2 == 0:
self.BuyMarket(volume)
self._entry_price = price
else:
self.SellMarket(volume)
self._entry_price = price
self._last_demo_entry_time = server_time
def _update_long_trailing(self, current_price):
entry_price = self._entry_price
if entry_price <= 0:
return
stop_offset = self._trailing_stop_pips.Value * self._point_value
step_offset = self._trailing_step_pips.Value * self._point_value
if stop_offset <= 0:
return
activation_offset = stop_offset + step_offset
if current_price - entry_price <= activation_offset:
return
threshold = current_price - activation_offset
if self._long_stop_price is None or self._long_stop_price < threshold:
new_stop = current_price - stop_offset
if new_stop > 0:
self._long_stop_price = new_stop
if self._long_stop_price is not None and current_price <= self._long_stop_price:
self._exit_long_position()
def _update_short_trailing(self, current_price):
entry_price = self._entry_price
if entry_price <= 0:
return
stop_offset = self._trailing_stop_pips.Value * self._point_value
step_offset = self._trailing_step_pips.Value * self._point_value
if stop_offset <= 0:
return
activation_offset = stop_offset + step_offset
if entry_price - current_price <= activation_offset:
return
threshold = current_price + activation_offset
if self._short_stop_price is None or self._short_stop_price > threshold:
new_stop = current_price + stop_offset
self._short_stop_price = new_stop
if self._short_stop_price is not None and current_price >= self._short_stop_price:
self._exit_short_position()
def _exit_long_position(self):
if self._exit_order_pending:
return
volume = abs(self.Position)
if volume <= 0:
return
self.SellMarket(volume)
self._exit_order_pending = True
def _exit_short_position(self):
if self._exit_order_pending:
return
volume = abs(self.Position)
if volume <= 0:
return
self.BuyMarket(volume)
self._exit_order_pending = True
def _reset_trailing_state(self):
self._long_stop_price = None
self._short_stop_price = None
self._exit_order_pending = False
self._entry_price = 0.0
def _calculate_adjusted_point(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if step <= 0:
return 1.0
decimals = self._count_decimals(step)
return step * 10.0 if decimals == 3 or decimals == 5 else step
def _count_decimals(self, value):
value = abs(value)
decimals = 0
while value != int(value) and decimals < 10:
value *= 10.0
decimals += 1
return decimals
def OnReseted(self):
super(breakeven_trailing_stop_tick_strategy, self).OnReseted()
self._point_value = 0.0
self._long_stop_price = None
self._short_stop_price = None
self._exit_order_pending = False
self._entry_price = 0.0
self._last_demo_entry_time = None
self._candle_count = 0
def CreateClone(self):
return breakeven_trailing_stop_tick_strategy()