Stop Trailing Virtual
Esta estrategia emula un stop trailing virtual para posiciones largas y cortas. No genera señales de entrada; las órdenes deben abrirse externamente o manualmente. Una vez que existe una posición, la estrategia mantiene un stop trailing que sigue el precio a medida que se mueve en una dirección favorable. Si el precio alcanza el nivel del stop, la posición se cierra a mercado.
Parámetros
StopLoss– distancia fija del stop-loss en pasos de precio.TakeProfit– distancia fija del take-profit en pasos de precio.TrailingStop– distancia desde el precio actual hasta el stop trailing.TrailingStart– beneficio mínimo en pasos de precio antes de que comience el trailing.TrailingStep– beneficio adicional mínimo requerido para mover el nivel trailing.CandleType– serie de velas utilizada para procesar los datos de precio.
Notas
La estrategia se suscribe a velas del tipo especificado y evalúa la lógica de trailing solo en velas cerradas.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Virtual risk manager. Never opens a position. Tracks externally executed
/// position average price and closes it if a finished candle reaches the
/// fixed stop, target, or a monotonically tightening virtual trailing stop.
/// All configured distances are security price steps.
/// </summary>
public class VirtualTrailingStopStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLoss, _takeProfit, _trailingStop, _trailingStart, _trailingStep;
private readonly StrategyParam<DataType> _candleType;
private decimal? _entryPrice, _bestPrice, _virtualStop;
private decimal _trackedPosition;
private Order _pending;
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
public decimal TrailingStop { get => _trailingStop.Value; set => _trailingStop.Value = value; }
public decimal TrailingStart { get => _trailingStart.Value; set => _trailingStart.Value = value; }
public decimal TrailingStep { get => _trailingStep.Value; set => _trailingStep.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public VirtualTrailingStopStrategy()
{
_stopLoss = Param(nameof(StopLoss), 0m).SetNotNegative()
.SetDisplay("Stop Loss", "Fixed distance from filled entry in price steps", "Risk");
_takeProfit = Param(nameof(TakeProfit), 0m).SetNotNegative()
.SetDisplay("Take Profit", "Fixed target distance in price steps", "Risk");
_trailingStop = Param(nameof(TrailingStop), 30m).SetNotNegative()
.SetDisplay("Trailing Stop", "Trailing distance in price steps", "Risk");
_trailingStart = Param(nameof(TrailingStart), 10m).SetNotNegative()
.SetDisplay("Trailing Start", "Required favorable movement in price steps", "Risk");
_trailingStep = Param(nameof(TrailingStep), 1m).SetGreaterThanZero()
.SetDisplay("Trailing Step", "Minimum improvement before tightening", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
.SetDisplay("Candle Type", "Processed finished candles", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
private void ResetTracking()
{
_entryPrice = _bestPrice = _virtualStop = null;
_trackedPosition = 0m;
_pending = null;
}
protected override void OnReseted()
{
base.OnReseted();
ResetTracking();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetTracking();
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
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 = _bestPrice = _virtualStop = null;
_trackedPosition = 0m;
return;
}
if (position.AveragePrice is not decimal price || price <= 0m)
return;
if (_entryPrice == price && _trackedPosition * volume > 0m)
{
_trackedPosition = volume;
return;
}
_entryPrice = _bestPrice = price;
_trackedPosition = volume;
_virtualStop = null;
}
private void Tighten(bool isLong, decimal candidate)
=> _virtualStop = _virtualStop is decimal old
? isLong ? Math.Max(old, candidate) : Math.Min(old, candidate)
: candidate;
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || Position == 0m || _entryPrice is not decimal entry ||
!IsFormedAndOnlineAndAllowTrading() ||
(_pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed)))
return;
var isLong = Position > 0m;
var price = candle.ClosePrice;
var step = Security.PriceStep is decimal positive && positive > 0m ? positive : 1m;
var best = _bestPrice ?? entry;
best = isLong ? Math.Max(best, price) : Math.Min(best, price);
_bestPrice = best;
if (TrailingStop > 0m)
{
var profit = isLong ? best - entry : entry - best;
if (profit >= TrailingStart * step)
{
var candidate = isLong ? best - TrailingStop * step : best + TrailingStop * step;
if (_virtualStop is not decimal existing ||
(isLong ? candidate >= existing + TrailingStep * step
: candidate <= existing - TrailingStep * step))
Tighten(isLong, candidate);
}
}
var fixedStop = StopLoss > 0m && (isLong ? price <= entry - StopLoss * step
: price >= entry + StopLoss * step);
var target = TakeProfit > 0m && (isLong ? price >= entry + TakeProfit * step
: price <= entry - TakeProfit * step);
var trailing = _virtualStop is decimal stop && (isLong ? price <= stop : price >= stop);
if (fixedStop || target || trailing)
_pending = isLong ? SellMarket(Math.Abs(Position)) : BuyMarket(Math.Abs(Position));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, OrderStates
from StockSharp.Algo.Strategies import Strategy
class virtual_trailing_stop_strategy(Strategy):
"""External-position-only virtual close-based stop, target and trailing manager."""
def __init__(self):
super(virtual_trailing_stop_strategy, self).__init__()
self._stop_loss = self.Param("StopLoss", 0.0).SetNotNegative().SetDisplay(
"Stop Loss", "Fixed distance from filled entry in price steps", "Risk")
self._take_profit = self.Param("TakeProfit", 0.0).SetNotNegative().SetDisplay(
"Take Profit", "Fixed target distance in price steps", "Risk")
self._trailing_stop = self.Param("TrailingStop", 30.0).SetNotNegative().SetDisplay(
"Trailing Stop", "Trailing distance in price steps", "Risk")
self._trailing_start = self.Param("TrailingStart", 10.0).SetNotNegative().SetDisplay(
"Trailing Start", "Required favorable movement in price steps", "Risk")
self._trailing_step = self.Param("TrailingStep", 1.0).SetGreaterThanZero().SetDisplay(
"Trailing Step", "Minimum improvement before tightening", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(8))).SetDisplay(
"Candle Type", "Processed finished candles", "General")
self._reset_tracking()
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _reset_tracking(self):
self._entry_price = self._best_price = self._virtual_stop = None
self._tracked_position = 0.0
self._pending = None
def OnReseted(self):
super(virtual_trailing_stop_strategy, self).OnReseted()
self._reset_tracking()
def OnStarted2(self, time):
super(virtual_trailing_stop_strategy, self).OnStarted2(time)
self._reset_tracking()
self.SubscribeCandles(self.CandleType).Bind(self.ProcessCandle).Start()
def OnNewPosition(self, position):
super(virtual_trailing_stop_strategy, self).OnNewPosition(position)
self._observe_position(position)
def OnPositionChanged(self, position):
super(virtual_trailing_stop_strategy, self).OnPositionChanged(position)
self._observe_position(position)
def OnPositionReceived(self, position):
super(virtual_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 = float(position.CurrentValue or 0)
if volume == 0:
self._entry_price = self._best_price = self._virtual_stop = None
self._tracked_position = 0.0
return
if position.AveragePrice is None or position.AveragePrice <= 0:
return
price = float(position.AveragePrice)
if self._entry_price == price and self._tracked_position * volume > 0:
self._tracked_position = volume
return
self._entry_price = self._best_price = price
self._virtual_stop = None
self._tracked_position = volume
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished or self.Position == 0 or self._entry_price is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed):
return
is_long = self.Position > 0
price = float(candle.ClosePrice)
entry = self._entry_price
step = float(self.Security.PriceStep or 1)
if step <= 0:
step = 1.0
best = self._best_price if self._best_price is not None else entry
best = max(best, price) if is_long else min(best, price)
self._best_price = best
distance = float(self._trailing_stop.Value) * step
if distance > 0:
profit = best - entry if is_long else entry - best
if profit >= float(self._trailing_start.Value) * step:
candidate = best - distance if is_long else best + distance
move = float(self._trailing_step.Value) * step
current = self._virtual_stop
if current is None or (candidate >= current + move if is_long else candidate <= current - move):
self._virtual_stop = (max(current, candidate) if is_long else min(current, candidate)) if current is not None else candidate
fixed_dist = float(self._stop_loss.Value) * step
take_dist = float(self._take_profit.Value) * step
fixed_stop = fixed_dist > 0 and (price <= entry - fixed_dist if is_long else price >= entry + fixed_dist)
target = take_dist > 0 and (price >= entry + take_dist if is_long else price <= entry - take_dist)
trail_hit = self._virtual_stop is not None and (price <= self._virtual_stop if is_long else price >= self._virtual_stop)
if fixed_stop or target or trail_hit:
self._pending = self.SellMarket(abs(self.Position)) if is_long else self.BuyMarket(abs(self.Position))
def CreateClone(self):
return virtual_trailing_stop_strategy()