Trail SL Manager 策略
概述
Trail SL Manager 是对 MetaTrader trailSL 脚本的迁移版本。
策略本身不会开仓,而是接管已有持仓,根据行情进展自动调整保护性止损。
它先在价格达到指定利润时将止损推至保本价,再按设定的步长逐级跟踪,从而持续锁定利润。
工作流程
- 订阅指定的 K 线类型,仅在 K 线收盘后处理数据。
- 读取当前持仓方向与平均建仓价,换算出已经获得的点数利润。
- 当利润达到
BreakEvenTriggerPoints时,将止损移动到建仓价,并可额外加入BreakEvenOffsetPoints点的缓冲。 - 若允许提前启动或已经完成保本,策略每当价格再移动
TrailStepPoints点时,就把止损再推进TrailOffsetPoints点;一旦行情回撤触发该价格,即以市价平仓。
所有计算均使用与原脚本相同的点数逻辑,方便从 MetaTrader 迁移到 StockSharp 的交易者继续保持熟悉的手感。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
EnableBreakEven |
是否启用保本移动。 | true |
BreakEvenTriggerPoints |
启动保本所需的盈利点数。 | 20 |
BreakEvenOffsetPoints |
保本时在建仓价基础上额外锁定的点数。 | 10 |
EnableTrailing |
是否启用跟踪止损。 | true |
TrailAfterBreakEven |
若为 true,仅在完成保本后才开始跟踪。 |
true |
TrailStartPoints |
开始跟踪前需要达到的最小盈利点数。 | 40 |
TrailStepPoints |
两次重新计算止损之间的盈利增量。 | 10 |
TrailOffsetPoints |
每次推进止损所增加的点数。 | 10 |
InitialStopPoints |
新建仓位时的初始保护止损距离。 | 200 |
CandleType |
用于监控行情的 K 线类型。 | 1 Minute |
使用方法
- 将策略加载到已经由其他策略或人工下单产生持仓的环境中。
- 按交易品种波动和经纪商限制配置各个点数阈值。
- 启动策略,使其在每根 K 线收盘后检查是否需要移动止损。
- 通过图表绘制观察策略执行情况,并根据需要与真实止损单配合使用。
提示: 策略在内部模拟跟踪止损,并在触发价被突破时发送市价单平仓。如需在交易所侧保留硬止损,请结合自身业务流程另行设置。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Utility strategy that mirrors the trailSL MetaTrader script.
/// It manages open positions by moving the protective stop to break even and trailing it as price advances.
/// The strategy does not generate its own entry signals.
/// </summary>
public class TrailSlManagerStrategy : Strategy
{
private readonly StrategyParam<bool> _enableBreakEven;
private readonly StrategyParam<int> _breakEvenTriggerPoints;
private readonly StrategyParam<int> _breakEvenOffsetPoints;
private readonly StrategyParam<bool> _enableTrailing;
private readonly StrategyParam<bool> _trailAfterBreakEven;
private readonly StrategyParam<int> _trailStartPoints;
private readonly StrategyParam<int> _trailStepPoints;
private readonly StrategyParam<int> _trailOffsetPoints;
private readonly StrategyParam<int> _initialStopPoints;
private readonly StrategyParam<DataType> _candleType;
private decimal _priceStep;
private decimal _longStop;
private decimal _shortStop;
private bool _longBreakEvenActive;
private bool _shortBreakEvenActive;
private decimal _lastEntryPrice;
private SimpleMovingAverage _smaFast;
private SimpleMovingAverage _smaSlow;
private int _cooldown;
private int _lastSignal;
/// <summary>
/// Enables automatic break-even adjustment.
/// </summary>
public bool EnableBreakEven
{
get => _enableBreakEven.Value;
set => _enableBreakEven.Value = value;
}
/// <summary>
/// Required profit in points before break-even is activated.
/// </summary>
public int BreakEvenTriggerPoints
{
get => _breakEvenTriggerPoints.Value;
set => _breakEvenTriggerPoints.Value = value;
}
/// <summary>
/// Additional points added to the break-even price once triggered.
/// </summary>
public int BreakEvenOffsetPoints
{
get => _breakEvenOffsetPoints.Value;
set => _breakEvenOffsetPoints.Value = value;
}
/// <summary>
/// Enables trailing stop management.
/// </summary>
public bool EnableTrailing
{
get => _enableTrailing.Value;
set => _enableTrailing.Value = value;
}
/// <summary>
/// Trailing starts only after a successful break-even move.
/// </summary>
public bool TrailAfterBreakEven
{
get => _trailAfterBreakEven.Value;
set => _trailAfterBreakEven.Value = value;
}
/// <summary>
/// Minimum profit in points before trailing begins.
/// </summary>
public int TrailStartPoints
{
get => _trailStartPoints.Value;
set => _trailStartPoints.Value = value;
}
/// <summary>
/// Distance in points between trailing recalculations.
/// </summary>
public int TrailStepPoints
{
get => _trailStepPoints.Value;
set => _trailStepPoints.Value = value;
}
/// <summary>
/// Stop loss increment applied on each trailing step (points).
/// </summary>
public int TrailOffsetPoints
{
get => _trailOffsetPoints.Value;
set => _trailOffsetPoints.Value = value;
}
/// <summary>
/// Initial protective stop distance measured in points.
/// </summary>
public int InitialStopPoints
{
get => _initialStopPoints.Value;
set => _initialStopPoints.Value = value;
}
/// <summary>
/// Candle type used for monitoring price progress.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public TrailSlManagerStrategy()
{
_enableBreakEven = Param(nameof(EnableBreakEven), true)
.SetDisplay("Break Even", "Enable break-even stop adjustment", "Risk")
;
_breakEvenTriggerPoints = Param(nameof(BreakEvenTriggerPoints), 20)
.SetDisplay("Break Even Trigger", "Points required before moving to break-even", "Risk")
;
_breakEvenOffsetPoints = Param(nameof(BreakEvenOffsetPoints), 10)
.SetDisplay("Break Even Offset", "Extra points locked when break-even triggers", "Risk")
;
_enableTrailing = Param(nameof(EnableTrailing), true)
.SetDisplay("Trailing", "Enable trailing stop management", "Risk")
;
_trailAfterBreakEven = Param(nameof(TrailAfterBreakEven), true)
.SetDisplay("Trail After Break Even", "Start trailing only after break-even", "Risk")
;
_trailStartPoints = Param(nameof(TrailStartPoints), 40)
.SetDisplay("Trail Start", "Points of profit before trailing is considered", "Risk")
;
_trailStepPoints = Param(nameof(TrailStepPoints), 10)
.SetDisplay("Trail Step", "Price step that triggers a new trailing recalculation", "Risk")
;
_trailOffsetPoints = Param(nameof(TrailOffsetPoints), 10)
.SetDisplay("Trail Offset", "Points added to the stop on every trailing step", "Risk")
;
_initialStopPoints = Param(nameof(InitialStopPoints), 200)
.SetDisplay("Initial Stop", "Initial stop distance used before trailing", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle subscription for monitoring", "Data");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
_priceStep = 0;
_longStop = 0;
_shortStop = 0;
_longBreakEvenActive = false;
_shortBreakEvenActive = false;
_lastEntryPrice = 0;
_smaFast = default;
_smaSlow = default;
_cooldown = 0;
_lastSignal = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_priceStep = Security?.PriceStep ?? 1m;
if (_priceStep <= 0m)
_priceStep = 1m;
_smaFast = new SimpleMovingAverage { Length = 10 };
_smaSlow = new SimpleMovingAverage { Length = 30 };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_smaFast, _smaSlow, ProcessCandleWithIndicators).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (Position == 0)
{
ResetState();
return;
}
if (trade.Order.Side == Sides.Buy && Position > 0)
{
_longStop = InitialStopPoints > 0 ? _lastEntryPrice - InitialStopPoints * _priceStep : 0m;
_longBreakEvenActive = false;
}
else if (trade.Order.Side == Sides.Sell && Position < 0)
{
_shortStop = InitialStopPoints > 0 ? _lastEntryPrice + InitialStopPoints * _priceStep : 0m;
_shortBreakEvenActive = false;
}
}
private void ProcessCandleWithIndicators(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldown > 0)
{
_cooldown--;
}
else
{
var signal = fast > slow ? 1 : fast < slow ? -1 : 0;
if (signal == 1 && _lastSignal != 1 && Position == 0)
{
BuyMarket();
_lastEntryPrice = candle.ClosePrice;
_lastSignal = 1;
_cooldown = 20;
}
else if (signal == -1 && _lastSignal != -1 && Position == 0)
{
SellMarket();
_lastEntryPrice = candle.ClosePrice;
_lastSignal = -1;
_cooldown = 20;
}
}
ManageLongPosition(candle);
ManageShortPosition(candle);
}
private void ManageLongPosition(ICandleMessage candle)
{
if (Position <= 0)
{
_longStop = 0m;
_longBreakEvenActive = false;
return;
}
var entryPrice = _lastEntryPrice;
if (entryPrice <= 0m)
return;
var currentPrice = candle.ClosePrice;
var profitPoints = (currentPrice - entryPrice) / _priceStep;
if (EnableBreakEven && !_longBreakEvenActive && profitPoints >= BreakEvenTriggerPoints && BreakEvenTriggerPoints > 0)
{
var newStop = BreakEvenOffsetPoints > 0
? entryPrice + BreakEvenOffsetPoints * _priceStep
: entryPrice;
if (newStop < currentPrice)
{
_longStop = Math.Max(_longStop, newStop);
_longBreakEvenActive = true;
}
}
if (!EnableTrailing || TrailOffsetPoints <= 0 || TrailStepPoints <= 0)
return;
var requireBreakEven = TrailAfterBreakEven && EnableBreakEven;
if (requireBreakEven && !_longBreakEvenActive)
return;
var baseStop = requireBreakEven
? (_longStop > 0m ? _longStop : (BreakEvenOffsetPoints > 0 ? entryPrice + BreakEvenOffsetPoints * _priceStep : entryPrice))
: (InitialStopPoints > 0 ? entryPrice - InitialStopPoints * _priceStep : (_longStop > 0m ? _longStop : 0m));
if (baseStop <= 0m)
return;
if (!requireBreakEven && profitPoints < TrailStartPoints)
return;
if (requireBreakEven)
{
var baseDistance = (currentPrice - baseStop) / _priceStep;
if (baseDistance < TrailStartPoints)
return;
}
var startPrice = requireBreakEven
? baseStop + (TrailStartPoints - TrailStepPoints) * _priceStep
: entryPrice + (TrailStartPoints - TrailStepPoints) * _priceStep;
var stepDistance = TrailStepPoints * _priceStep;
if (stepDistance <= 0m)
return;
var openSteps = (currentPrice - startPrice) / stepDistance;
if (openSteps <= 0m)
return;
var stepOpenPrice = (int)Math.Floor(openSteps);
var currentStopSteps = _longStop > baseStop
? (int)Math.Floor((_longStop - baseStop) / (TrailOffsetPoints * _priceStep))
: 0;
if (stepOpenPrice <= currentStopSteps)
return;
var proposedStop = baseStop + stepOpenPrice * TrailOffsetPoints * _priceStep;
var maxStop = candle.LowPrice - _priceStep;
if (proposedStop >= maxStop)
proposedStop = maxStop;
if (proposedStop > _longStop && proposedStop < currentPrice)
_longStop = proposedStop;
if (_longStop > 0m && candle.LowPrice <= _longStop)
SellMarket(Position);
}
private void ManageShortPosition(ICandleMessage candle)
{
if (Position >= 0)
{
_shortStop = 0m;
_shortBreakEvenActive = false;
return;
}
var entryPrice = _lastEntryPrice;
if (entryPrice <= 0m)
return;
var currentPrice = candle.ClosePrice;
var profitPoints = (entryPrice - currentPrice) / _priceStep;
if (EnableBreakEven && !_shortBreakEvenActive && profitPoints >= BreakEvenTriggerPoints && BreakEvenTriggerPoints > 0)
{
var newStop = BreakEvenOffsetPoints > 0
? entryPrice - BreakEvenOffsetPoints * _priceStep
: entryPrice;
if (newStop > currentPrice)
{
_shortStop = _shortStop == 0m ? newStop : Math.Min(_shortStop, newStop);
_shortBreakEvenActive = true;
}
}
if (!EnableTrailing || TrailOffsetPoints <= 0 || TrailStepPoints <= 0)
return;
var requireBreakEven = TrailAfterBreakEven && EnableBreakEven;
if (requireBreakEven && !_shortBreakEvenActive)
return;
var baseStop = requireBreakEven
? (_shortStop > 0m ? _shortStop : (BreakEvenOffsetPoints > 0 ? entryPrice - BreakEvenOffsetPoints * _priceStep : entryPrice))
: (InitialStopPoints > 0 ? entryPrice + InitialStopPoints * _priceStep : (_shortStop > 0m ? _shortStop : 0m));
if (baseStop <= 0m)
return;
if (!requireBreakEven && profitPoints < TrailStartPoints)
return;
if (requireBreakEven)
{
var baseDistance = (baseStop - currentPrice) / _priceStep;
if (baseDistance < TrailStartPoints)
return;
}
var startPrice = requireBreakEven
? baseStop - (TrailStartPoints - TrailStepPoints) * _priceStep
: entryPrice - (TrailStartPoints - TrailStepPoints) * _priceStep;
var stepDistance = TrailStepPoints * _priceStep;
if (stepDistance <= 0m)
return;
var openSteps = (startPrice - currentPrice) / stepDistance;
if (openSteps <= 0m)
return;
var stepOpenPrice = (int)Math.Floor(openSteps);
var currentStopSteps = _shortStop > 0m
? (int)Math.Floor((baseStop - _shortStop) / (TrailOffsetPoints * _priceStep))
: 0;
if (stepOpenPrice <= currentStopSteps)
return;
var proposedStop = baseStop - stepOpenPrice * TrailOffsetPoints * _priceStep;
var minStop = candle.HighPrice + _priceStep;
if (proposedStop <= minStop)
proposedStop = minStop;
if ((_shortStop == 0m || proposedStop < _shortStop) && proposedStop > currentPrice)
_shortStop = proposedStop;
if (_shortStop > 0m && candle.HighPrice >= _shortStop)
BuyMarket(Math.Abs(Position));
}
private void ResetState()
{
_longStop = 0m;
_shortStop = 0m;
_longBreakEvenActive = false;
_shortBreakEvenActive = false;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class trail_sl_manager_strategy(Strategy):
"""SMA crossover (10/30) with break-even and trailing stop management."""
def __init__(self):
super(trail_sl_manager_strategy, self).__init__()
self._enable_be = self.Param("EnableBreakEven", True).SetDisplay("Break Even", "Enable break-even stop adjustment", "Risk")
self._be_trigger = self.Param("BreakEvenTriggerPoints", 20).SetDisplay("Break Even Trigger", "Points before break-even", "Risk")
self._be_offset = self.Param("BreakEvenOffsetPoints", 10).SetDisplay("Break Even Offset", "Extra points locked at break-even", "Risk")
self._enable_trailing = self.Param("EnableTrailing", True).SetDisplay("Trailing", "Enable trailing stop management", "Risk")
self._trail_after_be = self.Param("TrailAfterBreakEven", True).SetDisplay("Trail After Break Even", "Start trailing only after break-even", "Risk")
self._trail_start = self.Param("TrailStartPoints", 40).SetDisplay("Trail Start", "Points before trailing begins", "Risk")
self._trail_step = self.Param("TrailStepPoints", 10).SetDisplay("Trail Step", "Step for trailing recalculation", "Risk")
self._trail_offset = self.Param("TrailOffsetPoints", 10).SetDisplay("Trail Offset", "SL increment per trailing step", "Risk")
self._initial_stop = self.Param("InitialStopPoints", 200).SetDisplay("Initial Stop", "Initial stop distance", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candle subscription", "Data")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def _reset_state(self):
self._long_stop = 0.0
self._short_stop = 0.0
self._long_be_active = False
self._short_be_active = False
def OnReseted(self):
super(trail_sl_manager_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(trail_sl_manager_strategy, self).OnStarted2(time)
sec = self.Security
ps = 0.0
if sec is not None and sec.PriceStep is not None:
try:
ps = float(sec.PriceStep)
except:
ps = 0.0
self._price_step = ps if ps > 0 else 1.0
self._last_entry_price = 0.0
self._reset_state()
fast = SimpleMovingAverage()
fast.Length = 10
slow = SimpleMovingAverage()
slow.Length = 30
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(fast, slow, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnOwnTradeReceived(self, trade):
super(trail_sl_manager_strategy, self).OnOwnTradeReceived(trade)
if self.Position == 0:
self._reset_state()
return
if trade.Order.Side == Sides.Buy and self.Position > 0:
self._long_stop = self._last_entry_price - self._initial_stop.Value * self._price_step if self._initial_stop.Value > 0 else 0.0
self._long_be_active = False
elif trade.Order.Side == Sides.Sell and self.Position < 0:
self._short_stop = self._last_entry_price + self._initial_stop.Value * self._price_step if self._initial_stop.Value > 0 else 0.0
self._short_be_active = False
def OnProcess(self, candle, fast_val, slow_val):
if candle.State != CandleStates.Finished:
return
fast = float(fast_val)
slow = float(slow_val)
if fast > slow and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._last_entry_price = float(candle.ClosePrice)
elif fast < slow and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._last_entry_price = float(candle.ClosePrice)
self._manage_long(candle)
self._manage_short(candle)
def _manage_long(self, candle):
if self.Position <= 0:
self._long_stop = 0.0
self._long_be_active = False
return
entry_price = self._last_entry_price
if entry_price <= 0:
return
ps = self._price_step
current_price = float(candle.ClosePrice)
profit_points = (current_price - entry_price) / ps
be_trigger = self._be_trigger.Value
be_offset = self._be_offset.Value
if self._enable_be.Value and not self._long_be_active and profit_points >= be_trigger and be_trigger > 0:
new_stop = entry_price + be_offset * ps if be_offset > 0 else entry_price
if new_stop < current_price:
self._long_stop = max(self._long_stop, new_stop)
self._long_be_active = True
trail_offset = self._trail_offset.Value
trail_step = self._trail_step.Value
if not self._enable_trailing.Value or trail_offset <= 0 or trail_step <= 0:
return
require_be = self._trail_after_be.Value and self._enable_be.Value
if require_be and not self._long_be_active:
return
if require_be:
base_stop = self._long_stop if self._long_stop > 0 else (entry_price + be_offset * ps if be_offset > 0 else entry_price)
else:
if self._initial_stop.Value > 0:
base_stop = entry_price - self._initial_stop.Value * ps
else:
base_stop = self._long_stop if self._long_stop > 0 else 0.0
if base_stop <= 0:
return
trail_start = self._trail_start.Value
if not require_be and profit_points < trail_start:
return
if require_be:
base_distance = (current_price - base_stop) / ps
if base_distance < trail_start:
return
if require_be:
start_price = base_stop + (trail_start - trail_step) * ps
else:
start_price = entry_price + (trail_start - trail_step) * ps
step_distance = trail_step * ps
if step_distance <= 0:
return
open_steps = (current_price - start_price) / step_distance
if open_steps <= 0:
return
step_open_price = int(math.floor(open_steps))
if self._long_stop > base_stop:
current_stop_steps = int(math.floor((self._long_stop - base_stop) / (trail_offset * ps)))
else:
current_stop_steps = 0
if step_open_price <= current_stop_steps:
return
proposed_stop = base_stop + step_open_price * trail_offset * ps
max_stop = float(candle.LowPrice) - ps
if proposed_stop >= max_stop:
proposed_stop = max_stop
if proposed_stop > self._long_stop and proposed_stop < current_price:
self._long_stop = proposed_stop
if self._long_stop > 0 and float(candle.LowPrice) <= self._long_stop:
self.SellMarket()
def _manage_short(self, candle):
if self.Position >= 0:
self._short_stop = 0.0
self._short_be_active = False
return
entry_price = self._last_entry_price
if entry_price <= 0:
return
ps = self._price_step
current_price = float(candle.ClosePrice)
profit_points = (entry_price - current_price) / ps
be_trigger = self._be_trigger.Value
be_offset = self._be_offset.Value
if self._enable_be.Value and not self._short_be_active and profit_points >= be_trigger and be_trigger > 0:
new_stop = entry_price - be_offset * ps if be_offset > 0 else entry_price
if new_stop > current_price:
self._short_stop = new_stop if self._short_stop == 0 else min(self._short_stop, new_stop)
self._short_be_active = True
trail_offset = self._trail_offset.Value
trail_step = self._trail_step.Value
if not self._enable_trailing.Value or trail_offset <= 0 or trail_step <= 0:
return
require_be = self._trail_after_be.Value and self._enable_be.Value
if require_be and not self._short_be_active:
return
if require_be:
base_stop = self._short_stop if self._short_stop > 0 else (entry_price - be_offset * ps if be_offset > 0 else entry_price)
else:
if self._initial_stop.Value > 0:
base_stop = entry_price + self._initial_stop.Value * ps
else:
base_stop = self._short_stop if self._short_stop > 0 else 0.0
if base_stop <= 0:
return
trail_start = self._trail_start.Value
if not require_be and profit_points < trail_start:
return
if require_be:
base_distance = (base_stop - current_price) / ps
if base_distance < trail_start:
return
if require_be:
start_price = base_stop - (trail_start - trail_step) * ps
else:
start_price = entry_price - (trail_start - trail_step) * ps
step_distance = trail_step * ps
if step_distance <= 0:
return
open_steps = (start_price - current_price) / step_distance
if open_steps <= 0:
return
step_open_price = int(math.floor(open_steps))
if self._short_stop > 0:
current_stop_steps = int(math.floor((base_stop - self._short_stop) / (trail_offset * ps)))
else:
current_stop_steps = 0
if step_open_price <= current_stop_steps:
return
proposed_stop = base_stop - step_open_price * trail_offset * ps
min_stop = float(candle.HighPrice) + ps
if proposed_stop <= min_stop:
proposed_stop = min_stop
if (self._short_stop == 0 or proposed_stop < self._short_stop) and proposed_stop > current_price:
self._short_stop = proposed_stop
if self._short_stop > 0 and float(candle.HighPrice) >= self._short_stop:
self.BuyMarket()
def CreateClone(self):
return trail_sl_manager_strategy()