トレーリングストップ・アクティベーション戦略
概要
トレーリングストップ・アクティベーション戦略は、既存のポジションの保護ストップレベルを管理します。エントリーシグナルは生成せず、代わりにポジションを開いた後にストップを調整して利益を確保します。
パラメーター
TrailingStop– トレーリングストップが有効になる前に、市場がポジションに有利な方向に動かなければならない価格単位の距離。StopLoss– 価格単位での初期ストップロス距離(オプション)。無効にするには0に設定。CandleType– 価格追跡に使用するローソク足の種類。
取引ルール
- ポジションが開かれたとき、
StopLossがゼロより大きければ初期ストップロスが設定されます。 - 利益が
TrailingStopを超えると、ストップレベルが指定された距離を維持しながら価格を追跡します。 - 価格がトレーリングストップのレベルに触れると、ポジションが決済されます。
- この戦略はロングおよびショートポジションの両方に機能します。
注意事項
この戦略はエントリーシグナルを提供する別の戦略と組み合わせて使用するよう設計されています。トレーリングストップによるエグジット管理のみに特化しています。
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()