Стратегия автоматического стоп-лосса и тейк-профита
Утилитарная стратегия автоматически выставляет защитные заявки стоп-лосса и тейк-профита для каждой открытой позиции по выбранному инструменту. Она повторяет поведение оригинального советника MetaTrader «AutoSet SL TP»: отслеживает список активных позиций и перед отправкой заявок проверяет соблюдение брокерских ограничений по расстоянию.
Стратегия не открывает сделки самостоятельно. Она наблюдает за объёмом, направлением и ценой исполнения позиций, которые созданы вручную либо другими алгоритмами. Как только появляется длинная или короткая позиция, алгоритм рассчитывает желаемые уровни стоп-лосса и тейк-профита в «метатрейдеровских» пунктах, корректирует их в соответствии с ограничениями Freeze/Stop, предоставленными торговой площадкой, и регистрирует соответствующие защитные заявки. После полного закрытия позиции выставленные стоп и тейк отменяются автоматически.
Алгоритм работы
Подписывается на данные Level1, чтобы получать лучшие цены bid/ask, а также необязательные поля StopLevel и FreezeLevel, которые публикует брокер.
Переводит заданные расстояния в пунктах в абсолютные цены с учётом биржевого шага цены и количества знаков. Котировки с тремя и пятью знаками масштабируются в десять раз, чтобы совпасть с трактовкой пунктов в MetaTrader.
При каждом обновлении котировок или появлении личной сделки:
Игнорирует событие, если нет открытой позиции или её направление не проходит по фильтру (только buy, только sell или оба варианта).
Вычисляет минимально допустимое расстояние от рыночной цены до защитной заявки. Если брокер не передаёт уровни Freeze/Stop, используется запас по умолчанию — три спреда, умноженные на 1.1.
Определяет цены стоп-лосса и тейк-профита относительно текущего ask (для длинных позиций) либо bid (для коротких), после чего нормализует значения по шагу цены инструмента.
Создаёт или пере-регистрирует защитные stop/limit-заявки точно на объём позиции. Перерегистрация выполняется только при изменении целевой цены или объёма, что сокращает количество модификаций на бирже.
При обнулении позиции все висящие защитные заявки отменяются. Если направление позиции перестаёт подходить под фильтр, заявки также снимаются.
Благодаря тому, что алгоритм ориентируется исключительно на внешние исполнения, его можно комбинировать с ручной торговлей, панелями и другими автоматическими системами, управляющими входами, — стратегия обеспечивает единый защитный контур.
Параметры
StopLossPips — расстояние от текущей цены до стоп-лосса в пунктах MetaTrader. Значение 0 отключает стоп-заявку. Значение по умолчанию: 50.
TakeProfitPips — расстояние до тейк-профита в пунктах MetaTrader. Значение 0 отключает тейк-профит. Значение по умолчанию: 140.
DirectionFilter — направление позиций, для которых выставляются защиты:
Buy — защищаются только длинные позиции.
Sell — защищаются только короткие позиции.
BuySell — защищаются обе стороны (поведение, совпадающее с исходным советником).
Практические замечания
Защитные заявки создаются на полный объём позиции. Если брокер ограничивает минимальный или максимальный лот, стратегия предварительно округляет объём к ближайшему допустимому значению.
Для обновления активных защитных заявок используется метод ReRegisterOrder, что позволяет по возможности сохранять исходный идентификатор заявки и избегать лишних отмен.
Запас по расстоянию (спред × 3 × 1.1) предотвращает нарушение скрытых ограничений площадки, когда явные значения Freeze/Stop отсутствуют.
Стратегию можно запускать до открытия позиций или после. Любая подходящая позиция, которая уже существует в момент запуска, будет защищена сразу после первого обновления котировок.
«Пункты» MetaTrader отличаются от биржевого шага цены на инструментах с тремя и пятью знаками. Стратегия повторяет логику исходного советника, умножая шаг точки, чтобы значения параметров полностью совпадали с настройками MT5.
Отличия от эксперта MetaTrader
Вместо модификации атрибутов стоп-лосса и тейк-профита внутри позиции StockSharp размещает явные защитные stop- и limit-заявки. Это делает работу алгоритма полностью прозрачной в стакане заявок.
Версия под StockSharp использует данные Level1 для восстановления ограничений брокера. Если провайдер публикует иные названия полей для Freeze/Stop, стратегия автоматически подбирает их из перечисления Level1Fields.
Все комментарии в коде и сообщения журнала выполнены на английском языке согласно требованиям репозитория, а документация доступна на русском и китайском языках для удобства пользователей.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
public class AutoSetStopLossTakeProfitStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public AutoSetStopLossTakeProfitStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
protected override void OnReseted()
{
base.OnReseted();
_fast = null; _slow = null;
_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished) return;
if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }
_prevFast = fastValue; _prevSlow = slowValue;
}
}
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
from StockSharp.Algo.Strategies import Strategy
class auto_set_stop_loss_take_profit_strategy(Strategy):
def __init__(self):
super(auto_set_stop_loss_take_profit_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 14) \
.SetDisplay("Fast Period", "Fast MA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 50) \
.SetDisplay("Slow Period", "Slow MA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(auto_set_stop_loss_take_profit_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(auto_set_stop_loss_take_profit_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 100
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return auto_set_stop_loss_take_profit_strategy()