Estrategia de Trailing Stop con Punto de Equilibrio por Ticks
Descripción general
Gestor de trailing stop basado en ticks convertido del asesor experto de MetaTrader e_Breakeven_v4.
Monitorea cada tick de operación para mover un stop-loss virtual una vez que el precio se desplaza suficientemente desde la entrada.
Cierra posiciones largas o cortas a mercado cuando se alcanza el nivel de trailing, replicando el comportamiento de punto de equilibrio más paso del EA original.
Incluye un modo demo opcional que abre posiciones aleatorias durante las pruebas para demostrar la lógica de trailing sin una fuente de señal externa.
Cómo funciona
La estrategia se suscribe a ticks de operación (DataType.Ticks) para emular el callback OnTick usado en MQL5.
Cuando existe una posición y el trailing stop (en pips) más el paso de trailing han sido superados, el nivel de stop se desplaza más cerca del precio.
Para posiciones largas, el stop se coloca en precio actual - trailing stop si el movimiento desde la entrada supera trailing stop + trailing step.
Para posiciones cortas, el stop se coloca en precio actual + trailing stop cuando el precio se mueve hacia abajo la misma distancia.
Si el precio en vivo toca o cruza el nivel de stop almacenado, la estrategia sale de toda la posición a mercado y reinicia el estado de trailing.
Una conversión interna de pips multiplica el paso de precio del broker por 10 cuando el instrumento tiene 3 o 5 dígitos decimales, coincidiendo con el ajuste punto-a-pip de MQL5.
Cuando el modo demo está habilitado, la estrategia abre una operación larga o corta aleatoria (usando el Volume configurado) la primera vez que llega un nuevo tick después de que la entrada anterior se cierra.
Parámetros
Nombre
Descripción
Predeterminado
Notas
TrailingStopPips
Distancia en pips entre el precio actual y el trailing stop.
10
Establecer en 0 para deshabilitar el trailing completamente.
TrailingStepPips
Distancia adicional en pips requerida antes de que el stop avance nuevamente.
1
Debe ser mayor que cero cuando el trailing stop está activo, reproduciendo la regla de validación del EA.
EnableDemoEntries
Habilita entradas aleatorias para backtests sin una señal externa.
false
Cuando true, la estrategia lanza una moneda en cada tick mientras está plana para decidir la dirección.
Reglas de gestión de posición
La estrategia no abre posiciones por sí misma a menos que EnableDemoEntries esté en true.
El trailing es simétrico para posiciones largas y cortas y funciona con cualquier tamaño de volumen.
Los niveles de stop se gestionan internamente (virtuales) y se aplican con salidas a mercado, evitando órdenes stop explícitas que pueden no ser compatibles con todos los conectores.
Cualquier operación manual o estrategia externa puede suministrar las entradas; este componente solo gestionará el trailing stop.
Notas de uso
Funciona mejor con instrumentos que proporcionan ticks de operación para que el trailing reaccione inmediatamente.
Asegúrese de que Volume esté configurado al tamaño de lote que corresponde a las posiciones entrantes si se usa el modo demo.
La conversión de pips asume precios estilo FX donde los símbolos con 3 o 5 decimales necesitan un multiplicador ×10 para convertir puntos en pips.
La salida se activa en el primer tick que cruza el precio de stop almacenado, coincidiendo con el flujo inmediato de modificación y cierre de la lógica MQL.
Diferencias respecto al experto MQL5 original
Usa stops virtuales con salidas a mercado en lugar de modificar órdenes stop-loss del lado del broker porque las estrategias StockSharp típicamente gestionan las salidas a través de la lógica de la estrategia.
Reemplaza el bloque de entrada aleatoria del tester de MetaTrader con el flag configurable EnableDemoEntries.
Convierte la lógica punto-a-pip usando Security.PriceStep y conteo de decimales en lugar de Symbol().Digits().
Todos los comentarios y registros son ahora en inglés de acuerdo con las pautas del repositorio.
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()