Стратегия Doubler с хеджирующим трейлингом — это перенос советника MetaTrader 5 Doubler.mq5 на высокоуровневый API StockSharp. При отсутствии позиций алгоритм мгновенно открывает симметричную пару рыночных заявок (покупка и продажа с одинаковым объёмом), после чего управляет каждой ногой отдельно с помощью стоп-лосса, тейк-профита и трейлинг-стопа. Все параметры заданы в пипсах и внутри переводятся в абсолютные цены через PriceStep, сохраняя поведение оригинального MQL-кода.
В отличие от направленных систем стратегия всегда старается держать две противоположные позиции. Каждая нога закрывается по собственным правилам защиты. Как только обе позиции закрыты, при следующем обновлении Level1 создаётся новая хеджированная пара, что позволяет постоянно находиться в рынке с нулевой чистой позицией.
Ключевые особенности
Автоматическое хеджирование — когда нет активных позиций и отложенных заявок, регистрируются две рыночные заявки (Buy и Sell) объёмом OrderVolume.
Риск-менеджмент в пипсах — стоп-лосс, тейк-профит и трейлинг задаются в пипсах. При расчёте используется PriceStep и число знаков (Decimals), поэтому инструменты с 3 или 5 знаками получают масштабирование ×10, как в MT5.
Независимый трейлинг для каждой ноги — для лонга контроль ведётся по лучшей цене Bid, для шорта — по Ask. Стоп переносится только если цена прошла не меньше TrailingStopPips + TrailingStepPips и новый уровень как минимум на TrailingStepPips ближе к рынку.
Проверка допустимого объёма — перед отправкой заявки объём сверяется с MinVolume, MaxVolume и VolumeStep. Нарушение ограничений вызывает исключение.
Подробные логи — при LogTradeDetails = true стратегия пишет информационные сообщения о сделках и передвижении трейлинга, что удобно для отладки.
Параметры
Параметр
Описание
Значение по умолчанию
Примечания
OrderVolume
Объём каждой ноги (Buy и Sell).
1
Должен соответствовать биржевым ограничениям; нормализуется к VolumeStep.
StopLossPips
Дистанция стоп-лосса в пипсах.
150
0 отключает стоп-лосс.
TakeProfitPips
Дистанция тейк-профита в пипсах.
300
0 отключает тейк-профит.
TrailingStopPips
Размер трейлинг-стопа в пипсах.
5
Если > 0, параметр TrailingStepPips обязан быть положительным.
TrailingStepPips
Дополнительное движение цены до переноса стопа.
5
Защищает от слишком частых переносов.
LogTradeDetails
Включить подробные логи.
false
Полезно при тестировании и наблюдении.
Логика работы
Вход в рынок
Стратегия подписывается на поток Level1 (best bid/ask).
При отсутствии активных позиций и незавершённых заявок отправляются две рыночные заявки одинакового объёма.
После получения сделок сохраняются цены входа, рассчитываются стартовые уровни защиты и сбрасывается состояние трейлинг-стопа.
Управление рисками
Стоп-лосс — если StopLossPips > 0, стоп устанавливается на расстоянии StopLossPips в пипсах от цены входа. Значение 0 выключает стоп-лосс.
Тейк-профит — аналогично рассчитывается по TakeProfitPips. Значение 0 отключает тейк.
Проверка объёма — метод NormalizeVolume гарантирует совместимость объёма с биржевыми ограничениями; при нарушении выбрасывается исключение.
Поведение трейлинг-стопа
Когда цена прошла в прибыльную сторону больше, чем TrailingStopPips + TrailingStepPips, новая точка стопа вычисляется как текущая цена ± TrailingStopPips.
Стоп переносится только если новый уровень ближе к цене как минимум на TrailingStepPips, либо если стоп ещё не был установлен.
Для лонга используется лучшая цена Bid, для шорта — лучшая цена Ask, что приближает вычисления к реальной цене исполнения.
Выход из позиции
Каждая нога закрывается собственной рыночной заявкой при срабатывании стопа, трейлинга или тейк-профита. После закрытия внутреннее состояние позиции очищается.
Когда обе ноги закрыты, ближайшее обновление Level1 инициирует новую пару заявок.
Требования к данным
Level1 (BestBid/BestAsk) — необходимы для отслеживания текущей цены, обновления трейлинг-стопа и проверок стоп/тейк уровней.
Дополнительные свечи или тиковые данные не требуются: стратегия полностью работает на Level1.
Примечания по конверсии
Пипсы автоматически переводятся в абсолютные цены через PriceStep; для инструментов с 3/5 знаками десятичной части применяется коэффициент 10.
Реализация использует только высокоуровневые методы Strategy (RegisterOrder, StartProtection, SubscribeLevel1) без обращения к низкоуровневым API.
Для отслеживания реальных и виртуальных позиций применяются объекты PositionState, что позволяет воспроизводить хеджирование даже в неттинговых портфелях.
Проект не требует изменения модулей тестирования в репозитории и может использоваться автономно.
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;
/// <summary>
/// Doubler strategy using double EMA confirmation with trailing stop management.
/// Enters long when both fast and medium EMAs are above slow EMA.
/// Enters short when both fast and medium EMAs are below slow EMA.
/// </summary>
public class DoublerStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _medPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _med;
private ExponentialMovingAverage _slow;
private decimal _entryPrice;
private int _cooldown;
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Medium EMA period.
/// </summary>
public int MedPeriod
{
get => _medPeriod.Value;
set => _medPeriod.Value = value;
}
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public DoublerStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_medPeriod = Param(nameof(MedPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Medium Period", "Medium EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 150)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fast = null;
_med = null;
_slow = null;
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_med = new ExponentialMovingAverage { Length = MedPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _med, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal medValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fast.IsFormed || !_med.IsFormed || !_slow.IsFormed)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
// Check SL/TP
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 100;
return;
}
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 100;
return;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 100;
return;
}
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 100;
return;
}
}
// Double confirmation: both fast and med above slow for long
if (fastValue > slowValue && medValue > slowValue && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
_cooldown = 100;
}
// Double confirmation: both fast and med below slow for short
else if (fastValue < slowValue && medValue < slowValue && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
_cooldown = 100;
}
}
}
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 doubler_strategy(Strategy):
def __init__(self):
super(doubler_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 20) \
.SetDisplay("Fast Period", "Fast EMA period", "Indicator")
self._med_period = self.Param("MedPeriod", 50) \
.SetDisplay("Medium Period", "Medium EMA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 200) \
.SetDisplay("Slow Period", "Slow EMA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 150) \
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 300) \
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk")
self._fast = None
self._med = None
self._slow = None
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def med_period(self):
return self._med_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(doubler_strategy, self).OnReseted()
self._fast = None
self._med = None
self._slow = None
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(doubler_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._med = ExponentialMovingAverage()
self._med.Length = self.med_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._med, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, med_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
med_val = float(med_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._med.IsFormed or not self._slow.IsFormed:
return
if self._cooldown > 0:
self._cooldown -= 1
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
# Check SL/TP
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
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
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
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
return
# Double confirmation: both fast and med above slow for long
if fast_val > slow_val and med_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 100
# Double confirmation: both fast and med below slow for short
elif fast_val < slow_val and med_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 100
def CreateClone(self):
return doubler_strategy()