Estratégia de Breakeven com Trailing Stop
Estratégia que demonstra como mover o stop loss para o ponto de equilíbrio (breakeven) e depois segui-lo conforme o preço avança. A estratégia entra em uma posição comprada e a gerencia em duas fases:
- Depois que o preço ganha
BreakevenPluspontos, o stop é movido paraBreakevenSteppontos acima da entrada. - Quando o preço continua com
TrailingPluspontos de lucro acima do stop, o stop segue o preço aTrailingSteppontos de distância.
A lógica é simétrica para posições vendidas, se uma for aberta manualmente.
Detalhes
- Critérios de entrada: Abre uma posição comprada na primeira vela concluída.
- Comprado/Vendido: Ambos (o exemplo usa comprado).
- Critérios de saída: O preço cruza o trailing stop.
- Stops: Breakeven e trailing stop.
- Valores padrão:
BreakevenPlus= 5BreakevenStep= 2TrailingPlus= 3TrailingStep= 1CandleType= TimeSpan.FromMinutes(1).TimeFrame()
- Filtros:
- Categoria: Gestão de stops
- Direção: Ambos
- Indicadores: Nenhum
- Stops: Breakeven, trailing
- Complexidade: Básico
- Período: Intradiário
- Sazonalidade: Não
- Redes neurais: Não
- Divergência: Não
- Nível de risco: Médio
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// One initial long entry on the first completed candle, followed by two-phase
/// breakeven/trailing stop management from the reported average execution price.
/// Manually attached short positions use symmetric exits, never automated short entries.
/// Distances are absolute price units, not percentages or price-step counts.
/// </summary>
public class BreakevenTrailingStopStrategy : Strategy
{
private readonly StrategyParam<decimal> _breakevenPlus;
private readonly StrategyParam<decimal> _breakevenStep;
private readonly StrategyParam<decimal> _trailingPlus;
private readonly StrategyParam<decimal> _trailingStep;
private readonly StrategyParam<DataType> _candleType;
private bool _initialEntryAttempted;
private bool _breakevenReached;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal _observedVolume;
private Order _pendingExit;
public decimal BreakevenPlus { get => _breakevenPlus.Value; set => _breakevenPlus.Value = value; }
public decimal BreakevenStep { get => _breakevenStep.Value; set => _breakevenStep.Value = value; }
public decimal TrailingPlus { get => _trailingPlus.Value; set => _trailingPlus.Value = value; }
public decimal TrailingStep { get => _trailingStep.Value; set => _trailingStep.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public BreakevenTrailingStopStrategy()
{
_breakevenPlus = Param(nameof(BreakevenPlus), 5m).SetGreaterThanZero()
.SetDisplay("Breakeven Plus", "Profit distance before breakeven in price units", "Risk");
_breakevenStep = Param(nameof(BreakevenStep), 2m).SetNotNegative()
.SetDisplay("Breakeven Step", "Stop offset past actual fill in price units", "Risk");
_trailingPlus = Param(nameof(TrailingPlus), 3m).SetGreaterThanZero()
.SetDisplay("Trailing Plus", "Price distance above current stop before trailing", "Risk");
_trailingStep = Param(nameof(TrailingStep), 1m).SetGreaterThanZero()
.SetDisplay("Trailing Step", "Price offset of ratcheted trailing stop", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe of entry candles", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_initialEntryAttempted = false;
_breakevenReached = false;
_entryPrice = null;
_stopPrice = null;
_observedVolume = 0m;
_pendingExit = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ProcessQuote).Start();
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || _initialEntryAttempted)
return;
// This demonstration enters at most once. An attached short position is left alone.
_initialEntryAttempted = true;
if (Position == 0m && IsFormedAndOnlineAndAllowTrading())
BuyMarket(Volume);
}
protected override void OnNewPosition(Position position)
{
base.OnNewPosition(position);
ObservePosition(position);
}
protected override void OnPositionChanged(Position position)
{
base.OnPositionChanged(position);
ObservePosition(position);
}
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
ObservePosition(position);
}
private void ObservePosition(Position position)
{
if (position.Security != Security || position.Portfolio != Portfolio)
return;
var volume = position.CurrentValue ?? 0m;
if (volume == 0m)
{
_entryPrice = _stopPrice = null;
_observedVolume = 0m;
_breakevenReached = false;
_pendingExit = null;
return;
}
if (position.AveragePrice is not decimal average || average <= 0m)
return;
if (average == _entryPrice && volume == _observedVolume)
return;
// A partial exit does not undo an already protected profit level.
if (average == _entryPrice && Math.Sign(volume) == Math.Sign(_observedVolume)
&& Math.Abs(volume) < Math.Abs(_observedVolume))
{
_observedVolume = volume;
return;
}
_entryPrice = average;
_observedVolume = volume;
_stopPrice = null;
_breakevenReached = false;
}
private void ProcessQuote(Level1ChangeMessage quote)
{
if (Position == 0m || _entryPrice is not decimal entry || !IsFormedAndOnlineAndAllowTrading())
return;
if (_pendingExit is not null && _pendingExit.State is not (OrderStates.Done or OrderStates.Failed))
return;
var isLong = Position > 0m;
var side = isLong ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
if (!quote.Changes.TryGetValue(side, out var raw) || raw is not decimal price || price <= 0m)
return;
if (!_breakevenReached)
{
if (isLong ? price >= entry + BreakevenPlus : price <= entry - BreakevenPlus)
{
_stopPrice = isLong ? entry + BreakevenStep : entry - BreakevenStep;
_breakevenReached = true;
}
}
else if (_stopPrice is decimal previous)
{
// Advance the stop using the current executable quote, never a previous candle wick.
if (isLong ? price >= previous + TrailingPlus : price <= previous - TrailingPlus)
{
var candidate = isLong ? price - TrailingStep : price + TrailingStep;
_stopPrice = isLong ? Math.Max(previous, candidate) : Math.Min(previous, candidate);
}
}
if (_stopPrice is not decimal stop || !(isLong ? price <= stop : price >= stop))
return;
var exit = new Order
{
Security = Security, Portfolio = Portfolio, Type = OrderTypes.Market,
Side = isLong ? Sides.Sell : Sides.Buy,
Volume = Math.Abs(Position), Comment = "Breakeven/trailing stop",
};
_pendingExit = exit;
RegisterOrder(exit);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Level1Fields, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class breakeven_trailing_stop_strategy(Strategy):
"""One initial long; price-unit breakeven and trailing manager for either held side."""
def __init__(self):
super(breakeven_trailing_stop_strategy, self).__init__()
self._breakeven_plus = self.Param("BreakevenPlus", 5.0).SetGreaterThanZero().SetDisplay(
"Breakeven Plus", "Price profit required before breakeven", "Risk")
self._breakeven_step = self.Param("BreakevenStep", 2.0).SetNotNegative().SetDisplay(
"Breakeven Step", "Price offset beyond average fill", "Risk")
self._trailing_plus = self.Param("TrailingPlus", 3.0).SetGreaterThanZero().SetDisplay(
"Trailing Plus", "Distance beyond current stop before trailing", "Risk")
self._trailing_step = self.Param("TrailingStep", 1.0).SetGreaterThanZero().SetDisplay(
"Trailing Step", "Distance behind executable price", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay(
"Candle Type", "Timeframe of entry candles", "General")
self._reset_state()
@property
def BreakevenPlus(self): return self._breakeven_plus.Value
@property
def BreakevenStep(self): return self._breakeven_step.Value
@property
def TrailingPlus(self): return self._trailing_plus.Value
@property
def TrailingStep(self): return self._trailing_step.Value
@property
def CandleType(self): return self._candle_type.Value
def _reset_state(self):
self._initial_entry_attempted = False
self._breakeven_reached = False
self._entry_price = None
self._stop_price = None
self._observed_volume = Decimal(0)
self._pending_exit = None
def OnReseted(self):
super(breakeven_trailing_stop_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(breakeven_trailing_stop_strategy, self).OnStarted2(time)
self._reset_state()
self.SubscribeCandles(self.CandleType).Bind(self._process_candle).Start()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
subscription = Subscription(DataType.Level1, self.Security)
subscription.MarketData.BuildField = field
self.SubscribeLevel1(subscription).Bind(self._process_quote).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished or self._initial_entry_attempted:
return
self._initial_entry_attempted = True
if self.Position == 0 and self.IsFormedAndOnlineAndAllowTrading():
self.BuyMarket(self.Volume)
def OnNewPosition(self, position):
super(breakeven_trailing_stop_strategy, self).OnNewPosition(position)
self._observe_position(position)
def OnPositionChanged(self, position):
super(breakeven_trailing_stop_strategy, self).OnPositionChanged(position)
self._observe_position(position)
def OnPositionReceived(self, position):
super(breakeven_trailing_stop_strategy, self).OnPositionReceived(position)
self._observe_position(position)
def _observe_position(self, position):
if position.Security != self.Security or position.Portfolio != self.Portfolio:
return
volume = Decimal(position.CurrentValue) if position.CurrentValue is not None else Decimal(0)
if volume == 0:
self._entry_price = self._stop_price = None
self._observed_volume = Decimal(0)
self._breakeven_reached = False
self._pending_exit = None
return
average = position.AveragePrice
if average is None or average <= 0:
return
average = Decimal(average)
if average == self._entry_price and volume == self._observed_volume:
return
# An actual partial exit may change CurrentValue, not the average fill.
if (average == self._entry_price and self._observed_volume * volume > 0
and abs(volume) < abs(self._observed_volume)):
self._observed_volume = volume
return
self._entry_price = average
self._observed_volume = volume
self._stop_price = None
self._breakeven_reached = False
def _process_quote(self, quote):
if self.Position == 0 or self._entry_price is None or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_exit is not None and self._pending_exit.State not in (OrderStates.Done, OrderStates.Failed):
return
long_side = self.Position > 0
side = Level1Fields.BestBidPrice if long_side else Level1Fields.BestAskPrice
if not quote.Changes.ContainsKey(side):
return
price = Decimal(quote.Changes[side])
if price <= 0:
return
entry = self._entry_price
if not self._breakeven_reached:
if (price >= entry + Decimal(self.BreakevenPlus)) if long_side else (price <= entry - Decimal(self.BreakevenPlus)):
offset = Decimal(self.BreakevenStep)
self._stop_price = entry + offset if long_side else entry - offset
self._breakeven_reached = True
elif self._stop_price is not None:
previous = self._stop_price
distance = Decimal(self.TrailingPlus)
if (price >= previous + distance) if long_side else (price <= previous - distance):
candidate = price - Decimal(self.TrailingStep) if long_side else price + Decimal(self.TrailingStep)
self._stop_price = max(previous, candidate) if long_side else min(previous, candidate)
if self._stop_price is None or not (price <= self._stop_price if long_side else price >= self._stop_price):
return
exit_order = Order()
exit_order.Security = self.Security
exit_order.Portfolio = self.Portfolio
exit_order.Type = OrderTypes.Market
exit_order.Side = Sides.Sell if long_side else Sides.Buy
exit_order.Volume = abs(self.Position)
exit_order.Comment = "Breakeven/trailing stop"
self._pending_exit = exit_order
self.RegisterOrder(exit_order)
def CreateClone(self):
return breakeven_trailing_stop_strategy()