Estratégia de Trailing Stop com Ponto de Equilíbrio por Ticks
Visão geral
Gerenciador de trailing stop baseado em ticks convertido do consultor especialista MetaTrader e_Breakeven_v4.
Monitora cada tick de negociação para mover um stop-loss virtual quando o preço se afasta o suficiente da entrada.
Fecha posições compradas ou vendidas a mercado quando o nível de trailing é atingido, replicando o comportamento de ponto de equilíbrio mais passo do EA original.
Inclui um modo demo opcional que abre posições aleatórias durante os testes para demonstrar a lógica de trailing sem uma fonte de sinal externa.
Como funciona
A estratégia assina ticks de negociação (DataType.Ticks) para emular o callback OnTick usado no MQL5.
Quando uma posição existe e o trailing stop (em pips) mais o passo de trailing foram excedidos, o nível de stop é deslocado mais perto do preço.
Para posições compradas, o stop é colocado em preço atual - trailing stop se o movimento desde a entrada exceder trailing stop + trailing step.
Para posições vendidas, o stop é colocado em preço atual + trailing stop quando o preço se move para baixo a mesma distância.
Se o preço ao vivo tocar ou cruzar o nível de stop armazenado, a estratégia sai de toda a posição a mercado e reinicia o estado de trailing.
Uma conversão interna de pips multiplica o passo de preço do broker por 10 quando o instrumento tem 3 ou 5 casas decimais, correspondendo ao ajuste ponto-a-pip do MQL5.
Quando o modo demo está habilitado, a estratégia abre uma negociação comprada ou vendida aleatoriamente (usando o Volume configurado) na primeira vez que um novo tick chega após a entrada anterior ser fechada.
Parâmetros
Nome
Descrição
Padrão
Notas
TrailingStopPips
Distância em pips entre o preço atual e o trailing stop.
10
Definir como 0 para desabilitar o trailing completamente.
TrailingStepPips
Distância adicional em pips necessária antes que o stop avance novamente.
1
Deve ser maior que zero quando o trailing stop está ativo, reproduzindo a regra de validação do EA.
EnableDemoEntries
Habilita entradas aleatórias para backtests sem um sinal externo.
false
Quando true, a estratégia lança uma moeda em cada tick enquanto está zerada para decidir a direção.
Regras de gerenciamento de posição
A estratégia não abre posições por si mesma a menos que EnableDemoEntries esteja em true.
O trailing é simétrico para posições compradas e vendidas e funciona com qualquer tamanho de volume.
Os níveis de stop são gerenciados internamente (virtuais) e aplicados com saídas a mercado, evitando ordens stop explícitas que podem não ser suportadas por todos os conectores.
Qualquer negociação manual ou estratégia externa pode fornecer as entradas; este componente só gerenciará o trailing stop.
Notas de uso
Funciona melhor com instrumentos que fornecem ticks de negociação para que o trailing reaja imediatamente.
Certifique-se de que Volume esteja configurado para o tamanho de lote que corresponde às posições recebidas se o modo demo for usado.
A conversão de pips assume preços no estilo FX onde símbolos com 3 ou 5 decimais precisam de um multiplicador ×10 para converter pontos em pips.
A saída é acionada no primeiro tick que cruza o preço de stop armazenado, correspondendo ao fluxo imediato de modificação e fechamento da lógica MQL.
Diferenças em relação ao especialista MQL5 original
Usa stops virtuais com saídas a mercado em vez de modificar ordens stop-loss do lado do broker porque as estratégias StockSharp tipicamente gerenciam saídas através da lógica da estratégia.
Substitui o bloco de entrada aleatória do testador MetaTrader pelo flag configurável EnableDemoEntries.
Converte a lógica ponto-a-pip usando Security.PriceStep e contagem de decimais em vez de Symbol().Digits().
Todos os comentários e registros agora estão em inglês de acordo com as diretrizes do repositório.
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()