动态止损
概述
原版 MetaTrader 专家顾问“Dynamic Stop Loss”并不会自行开仓,而是监控已有持仓。一旦出现新的 K 线,就把保护性止损重新放置在距离最新价格固定的距离处。StockSharp 版本保持相同的行为:每根完成的 K 线都会为当前持仓方向重新计算止损价位;当没有仓位时,策略保持空闲直到检测到新的仓位。
工作原理
- 策略根据
Candle Type参数订阅蜡烛(默认 1 分钟)。 - 蜡烛收盘时读取收盘价,并将用户设置的点数距离转换成绝对价格差。转换过程中优先使用
Security.PriceStep,若缺失则退回Security.Step,最后再退回常数1。 - 持有多头仓位时,策略取消已有的止损单,并在
Close - Distance位置挂出新的卖出止损单。 - 持有空头仓位时,策略在
Close + Distance位置挂出买入止损单。 - 当仓位被手动或止损单平掉后,会立即撤销剩余的保护性订单,避免留存的挂单。
因此止损始终围绕最新价格移动,与 MQL 版本一样,价格向不利方向波动时止损也会随之后退。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
StopLossPoints |
800 |
市价与止损之间的距离,单位为 MetaTrader 点。该数值会乘以 Security.PriceStep(缺省时依次退回 Security.Step 与 1)后再作用于收盘价。设为 0 可停用止损管理。 |
CandleType |
TimeFrameCandle(00:01:00) |
触发止损重算的蜡烛类型,应与 MetaTrader 中使用的周期保持一致。 |
使用说明
- 策略只负责管理止损,开仓动作需由其它策略或人工完成。
- 请确保证券的元数据(
PriceStep、Step、交易量信息)已正确填写,否则点值换算会偏离经纪商的最小报价步长。对于带有小数点报价的外汇品种尤其重要。 - 由于每根完成的 K 线都会重新计算止损,即使行情朝不利方向波动,止损仍会跟随价格移动,这与原始脚本中使用
OrderModify的方式一致。 - 每次计算出的价格都会替换旧的止损单,以确保交易平台与最新的保护价保持同步。
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Dynamic Stop Loss strategy: EMA trend with ATR-based dynamic stop management.
/// Enters on EMA trend direction, exits when price moves against by ATR distance.
/// </summary>
public class DynamicStopLossStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<int> _signalCooldownCandles;
private decimal _entryPrice;
private decimal _stopPrice;
private bool _prevAboveEma;
private bool _hasPrevSignal;
private int _candlesSinceTrade;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }
public DynamicStopLossStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_emaPeriod = Param(nameof(EmaPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA trend period", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR period for stop distance", "Indicators");
_atrMultiplier = Param(nameof(AtrMultiplier), 1.5m)
.SetDisplay("ATR Multiplier", "ATR multiplier for stop distance", "Risk");
_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0m;
_stopPrice = 0m;
_prevAboveEma = false;
_hasPrevSignal = false;
_candlesSinceTrade = SignalCooldownCandles;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
_stopPrice = 0;
_hasPrevSignal = false;
_candlesSinceTrade = SignalCooldownCandles;
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ema, atr, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal ema, decimal atr)
{
if (candle.State != CandleStates.Finished) return;
var close = candle.ClosePrice;
var stopDist = atr * AtrMultiplier;
var aboveEma = close > ema;
if (_candlesSinceTrade < SignalCooldownCandles)
_candlesSinceTrade++;
if (Position > 0)
{
var newStop = close - stopDist;
if (newStop > _stopPrice) _stopPrice = newStop;
if (close <= _stopPrice)
{
SellMarket();
_entryPrice = 0;
_stopPrice = 0;
_candlesSinceTrade = 0;
_prevAboveEma = aboveEma;
_hasPrevSignal = true;
return;
}
}
else if (Position < 0)
{
var newStop = close + stopDist;
if (newStop < _stopPrice || _stopPrice == 0) _stopPrice = newStop;
if (close >= _stopPrice)
{
BuyMarket();
_entryPrice = 0;
_stopPrice = 0;
_candlesSinceTrade = 0;
_prevAboveEma = aboveEma;
_hasPrevSignal = true;
return;
}
}
if (_hasPrevSignal && aboveEma != _prevAboveEma && _candlesSinceTrade >= SignalCooldownCandles)
{
if (aboveEma && Position <= 0)
{
BuyMarket();
_entryPrice = close;
_stopPrice = close - stopDist;
_candlesSinceTrade = 0;
}
else if (!aboveEma && Position >= 0)
{
SellMarket();
_entryPrice = close;
_stopPrice = close + stopDist;
_candlesSinceTrade = 0;
}
}
_prevAboveEma = aboveEma;
_hasPrevSignal = true;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class dynamic_stop_loss_strategy(Strategy):
def __init__(self):
super(dynamic_stop_loss_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 100) \
.SetDisplay("EMA Period", "EMA trend period", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "ATR period for stop distance", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 1.5) \
.SetDisplay("ATR Multiplier", "ATR multiplier for stop distance", "Risk")
self._signal_cooldown = self.Param("SignalCooldownCandles", 12) \
.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading")
self._ema = None
self._atr = None
self._entry_price = 0.0
self._stop_price = 0.0
self._prev_above_ema = False
self._has_prev_signal = False
self._candles_since_trade = 0
@property
def ema_period(self):
return self._ema_period.Value
@property
def atr_period(self):
return self._atr_period.Value
@property
def atr_multiplier(self):
return self._atr_multiplier.Value
@property
def signal_cooldown(self):
return self._signal_cooldown.Value
def OnReseted(self):
super(dynamic_stop_loss_strategy, self).OnReseted()
self._ema = None
self._atr = None
self._entry_price = 0.0
self._stop_price = 0.0
self._prev_above_ema = False
self._has_prev_signal = False
self._candles_since_trade = self.signal_cooldown
def OnStarted2(self, time):
super(dynamic_stop_loss_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = self.ema_period
self._atr = AverageTrueRange()
self._atr.Length = self.atr_period
self._entry_price = 0.0
self._stop_price = 0.0
self._has_prev_signal = False
self._candles_since_trade = self.signal_cooldown
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(30)))
subscription.Bind(self._ema, self._atr, self._process_candle)
subscription.Start()
def _process_candle(self, candle, ema_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not self._ema.IsFormed or not self._atr.IsFormed:
return
close = float(candle.ClosePrice)
ema_val = float(ema_value)
atr_val = float(atr_value)
stop_dist = atr_val * self.atr_multiplier
above_ema = close > ema_val
if self._candles_since_trade < self.signal_cooldown:
self._candles_since_trade += 1
if self.Position > 0:
new_stop = close - stop_dist
if new_stop > self._stop_price:
self._stop_price = new_stop
if close <= self._stop_price:
self.SellMarket()
self._entry_price = 0.0
self._stop_price = 0.0
self._candles_since_trade = 0
self._prev_above_ema = above_ema
self._has_prev_signal = True
return
elif self.Position < 0:
new_stop = close + stop_dist
if new_stop < self._stop_price or self._stop_price == 0.0:
self._stop_price = new_stop
if close >= self._stop_price:
self.BuyMarket()
self._entry_price = 0.0
self._stop_price = 0.0
self._candles_since_trade = 0
self._prev_above_ema = above_ema
self._has_prev_signal = True
return
if self._has_prev_signal and above_ema != self._prev_above_ema and self._candles_since_trade >= self.signal_cooldown:
if above_ema and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
self._stop_price = close - stop_dist
self._candles_since_trade = 0
elif not above_ema and self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._stop_price = close + stop_dist
self._candles_since_trade = 0
self._prev_above_ema = above_ema
self._has_prev_signal = True
def CreateClone(self):
return dynamic_stop_loss_strategy()