Estratégia de Ativação de Trailing Stop
Visão geral
A Estratégia de Ativação de Trailing Stop gerencia os níveis de stop de proteção para posições existentes. Ela não gera sinais de entrada; em vez disso, ajusta os stops após a abertura de uma posição para travar lucros.
Parâmetros
TrailingStop– distância em unidades de preço que o mercado deve se mover a favor da posição antes que um trailing stop seja ativado.StopLoss– distância inicial de stop-loss em unidades de preço (opcional). Defina como0para desativar.CandleType– tipo de candles usado para rastreamento de preço.
Regras de operação
- Quando uma posição é aberta, um stop-loss inicial é colocado se
StopLossfor maior que zero. - Uma vez que o lucro excede
TrailingStop, o nível de stop acompanha o preço mantendo a distância especificada. - A posição é fechada quando o preço toca o nível de trailing stop.
- A estratégia funciona tanto para posições compradas quanto vendidas.
Notas
Esta estratégia foi projetada para ser usada junto com outra estratégia que fornece sinais de entrada. Ela se concentra exclusivamente na gestão de saídas por meio de trailing stops.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Exit-only manager for an existing position. It never submits an entry.
/// Initial risk and trailing activation are anchored to the reported average fill price;
/// executable-side Level1 quotes trigger the market exit, not an old candle's high/low.
/// </summary>
public class TrailingStopActivationStrategy : Strategy
{
private readonly StrategyParam<decimal> _trailingStop;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private decimal? _entryPrice;
private decimal? _extremePrice;
private decimal? _stopPrice;
private decimal _trackedVolume;
private Order _pendingExit;
public decimal TrailingStop { get => _trailingStop.Value; set => _trailingStop.Value = value; }
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TrailingStopActivationStrategy()
{
_trailingStop = Param(nameof(TrailingStop), 500m).SetGreaterThanZero()
.SetDisplay("Trailing Stop", "Profit required to activate, and trailing distance in price units", "Risk");
_stopLoss = Param(nameof(StopLoss), 0m).SetNotNegative()
.SetDisplay("Initial Stop Loss", "Distance from actual entry price; zero disables it", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Optional chart candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, DataType.Level1), (Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = _extremePrice = _stopPrice = null;
_trackedVolume = 0m;
_pendingExit = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var subscription = new Subscription(DataType.Level1, Security);
subscription.MarketData.BuildField = field;
SubscribeLevel1(subscription).Bind(ProcessQuote).Start();
}
// CandleType remains a configurable visualization stream; risk is quote driven.
var candles = SubscribeCandles(CandleType);
candles.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candles);
DrawOwnTrades(area);
}
}
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 = _extremePrice = _stopPrice = null;
_trackedVolume = 0m;
_pendingExit = null;
return;
}
// A missing average fill is not a price quote: do not invent a stop anchor.
var average = position.AveragePrice;
if (average is not > 0m)
return;
if (_entryPrice == average && _trackedVolume == volume)
return;
// A partial reduction of the same-side position must never discard an
// already tightened stop; only added exposure/new average reanchors.
if (_entryPrice == average && Math.Sign(volume) == Math.Sign(_trackedVolume)
&& Math.Abs(volume) < Math.Abs(_trackedVolume))
{
_trackedVolume = volume;
return;
}
_entryPrice = average;
_trackedVolume = volume;
_extremePrice = average;
_stopPrice = StopLoss > 0m
? volume > 0m ? average - StopLoss : average + StopLoss
: null;
}
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;
// A long can be liquidated only at the bid, a short only at the ask.
var field = Position > 0m ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
if (!quote.Changes.TryGetValue(field, out var raw) || raw is not decimal price || price <= 0m)
return;
var longPosition = Position > 0m;
var extreme = _extremePrice ?? entry;
_extremePrice = extreme = longPosition ? Math.Max(extreme, price) : Math.Min(extreme, price);
// TrailingStop is both activation profit and the distance from the best executable quote.
if (longPosition ? extreme - entry > TrailingStop : entry - extreme > TrailingStop)
{
var candidate = longPosition ? extreme - TrailingStop : extreme + TrailingStop;
_stopPrice = _stopPrice is decimal old
? longPosition ? Math.Max(old, candidate) : Math.Min(old, candidate)
: candidate;
}
if (_stopPrice is not decimal stop || !(longPosition ? price <= stop : price >= stop))
return;
var exit = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = longPosition ? Sides.Sell : Sides.Buy,
Volume = Math.Abs(Position),
Comment = "Trailing/initial stop activation",
};
_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, Level1Fields, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class trailing_stop_activation_strategy(Strategy):
"""Exit-only manager: use reported position average and fresh executable quotes."""
def __init__(self):
super(trailing_stop_activation_strategy, self).__init__()
self._trailing_stop = self.Param("TrailingStop", 500.0).SetGreaterThanZero().SetDisplay(
"Trailing Stop", "Profit required to activate, and trailing price distance", "Risk")
self._stop_loss = self.Param("StopLoss", 0.0).SetNotNegative().SetDisplay(
"Initial Stop Loss", "Distance from filled entry; zero disables it", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay(
"Candle Type", "Optional chart candle timeframe", "General")
self._entry_price = None
self._extreme_price = None
self._stop_price = None
self._tracked_volume = Decimal(0)
self._pending_exit = None
@property
def TrailingStop(self):
return self._trailing_stop.Value
@TrailingStop.setter
def TrailingStop(self, value):
self._trailing_stop.Value = value
@property
def StopLoss(self):
return self._stop_loss.Value
@StopLoss.setter
def StopLoss(self, value):
self._stop_loss.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def GetWorkingSecurities(self):
return [(self.Security, DataType.Level1), (self.Security, self.CandleType)]
def OnReseted(self):
super(trailing_stop_activation_strategy, self).OnReseted()
self._entry_price = self._extreme_price = self._stop_price = None
self._tracked_volume = Decimal(0)
self._pending_exit = None
def OnStarted2(self, time):
super(trailing_stop_activation_strategy, self).OnStarted2(time)
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()
candles = self.SubscribeCandles(self.CandleType)
candles.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawOwnTrades(area)
def OnNewPosition(self, position):
super(trailing_stop_activation_strategy, self).OnNewPosition(position)
self._observe_position(position)
def OnPositionChanged(self, position):
super(trailing_stop_activation_strategy, self).OnPositionChanged(position)
self._observe_position(position)
def OnPositionReceived(self, position):
super(trailing_stop_activation_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 = position.CurrentValue if position.CurrentValue is not None else Decimal(0)
if volume == 0:
self._entry_price = self._extreme_price = self._stop_price = None
self._tracked_volume = Decimal(0)
self._pending_exit = None
return
average = position.AveragePrice
if average is None or average <= 0:
return
if self._entry_price == average and self._tracked_volume == volume:
return
# Preserve an achieved stop on a partial same-side reduction.
if (self._entry_price == average and self._tracked_volume * volume > 0
and abs(volume) < abs(self._tracked_volume)):
self._tracked_volume = volume
return
self._entry_price = average
self._tracked_volume = volume
self._extreme_price = average
distance = Decimal(self.StopLoss)
self._stop_price = ((average - distance) if volume > 0 else (average + distance)) if distance > 0 else None
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
is_long = self.Position > 0
field = Level1Fields.BestBidPrice if is_long else Level1Fields.BestAskPrice
if not quote.Changes.ContainsKey(field):
return
price = Decimal(quote.Changes[field])
if price <= 0:
return
entry = self._entry_price
extreme = self._extreme_price if self._extreme_price is not None else entry
extreme = max(extreme, price) if is_long else min(extreme, price)
self._extreme_price = extreme
distance = Decimal(self.TrailingStop)
if (extreme - entry > distance) if is_long else (entry - extreme > distance):
candidate = extreme - distance if is_long else extreme + distance
if self._stop_price is None:
self._stop_price = candidate
else:
self._stop_price = max(self._stop_price, candidate) if is_long else min(self._stop_price, candidate)
if self._stop_price is None or not (price <= self._stop_price if is_long 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 is_long else Sides.Buy
exit_order.Volume = abs(self.Position)
exit_order.Comment = "Trailing/initial stop activation"
self._pending_exit = exit_order
self.RegisterOrder(exit_order)
def CreateClone(self):
return trailing_stop_activation_strategy()