Открыть на GitHub

Стратегия Doubler с хеджирующим трейлингом

Обзор

Стратегия 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 Полезно при тестировании и наблюдении.

Логика работы

Вход в рынок

  1. Стратегия подписывается на поток Level1 (best bid/ask).
  2. При отсутствии активных позиций и незавершённых заявок отправляются две рыночные заявки одинакового объёма.
  3. После получения сделок сохраняются цены входа, рассчитываются стартовые уровни защиты и сбрасывается состояние трейлинг-стопа.

Управление рисками

  • Стоп-лосс — если StopLossPips > 0, стоп устанавливается на расстоянии StopLossPips в пипсах от цены входа. Значение 0 выключает стоп-лосс.
  • Тейк-профит — аналогично рассчитывается по TakeProfitPips. Значение 0 отключает тейк.
  • Проверка объёма — метод NormalizeVolume гарантирует совместимость объёма с биржевыми ограничениями; при нарушении выбрасывается исключение.

Поведение трейлинг-стопа

  1. Когда цена прошла в прибыльную сторону больше, чем TrailingStopPips + TrailingStepPips, новая точка стопа вычисляется как текущая цена ± TrailingStopPips.
  2. Стоп переносится только если новый уровень ближе к цене как минимум на TrailingStepPips, либо если стоп ещё не был установлен.
  3. Для лонга используется лучшая цена 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;
		}
	}
}