Estrategia de Activación de Trailing Stop
Descripción general
La Estrategia de Activación de Trailing Stop gestiona los niveles de stop de protección para las posiciones existentes. No genera señales de entrada; en cambio, ajusta los stops después de que se abre una posición para asegurar las ganancias.
Parámetros
TrailingStop– distancia en unidades de precio que el mercado debe moverse a favor de la posición antes de que se active el trailing stop.StopLoss– distancia inicial de stop-loss en unidades de precio (opcional). Establezca0para desactivar.CandleType– tipo de velas usadas para el seguimiento del precio.
Reglas de operación
- Cuando se abre una posición, se coloca un stop-loss inicial si
StopLosses mayor que cero. - Una vez que el beneficio supera
TrailingStop, el nivel de stop sigue al precio manteniendo la distancia especificada. - La posición se cierra cuando el precio toca el nivel de trailing stop.
- La estrategia funciona tanto para posiciones largas como cortas.
Notas
Esta estrategia está diseñada para usarse junto a otra estrategia que proporcione señales de entrada. Se centra únicamente en la gestión de salidas mediante 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()