Открыть на GitHub

Сетка-хедж стратегия Urdala Trol

Обзор

Urdala Trol Hedging Grid Strategy — прямой перенос советника MetaTrader 5 Urdala_Trol.mq5 на высокоуровневый API StockSharp. Стратегия постоянно удерживает позиции в обе стороны и наращивает объёмы по принципу мартингейла, когда срабатывают стоп-приказы. Для работы достаточно данных Level1 (лучшие bid/ask); индикаторы не используются.

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

  1. Начальный хедж (шаг 0). При отсутствии позиций стратегия сразу открывает рыночные сделки Buy и Sell с объёмом Base Volume.
  2. Доливка убыточной стороны (шаг 1.2). Если открыты только покупки или только продажи и самая убыточная сделка находится минимум на Grid Step пипсов от текущей цены, добавляется ещё одна позиция в том же направлении. Новый объём равен объёму худшей сделки плюс Min Lots Multiplier * minVolumeStep, где minVolumeStep берётся из VolumeStep или MinVolume инструмента.
  3. Обработка стоп-лосса (шаг 1.1). Когда позиция закрывается по стоп-лоссу (включая подтягиваемый стоп) с отрицательным результатом, стратегия повторно входит в том же направлении, если нет открытой сделки ближе Min Nearest пипсов к цене выхода.
  4. Реакция на прибыльный стоп (шаг 2.1). Если стоп закрыл позицию в плюс, немедленно открывается противоположная сделка с увеличенным объёмом.
  5. Трейлинг-стоп. После движения цены на Trailing Stop + Trailing Step пипсов в прибыльную сторону стоп переносится так, чтобы оставаться на расстоянии Trailing Stop пипсов. Трейлинг активен только если оба параметра больше нуля.

Все расстояния в пипсах переводятся в абсолютные цены через PriceStep инструмента. Для трёх- и пятизначных котировок шаг умножается на десять, повторяя логику «adjusted point» из оригинального советника.

Параметры

Параметр Значение по умолчанию Описание
BaseVolume 0.1 Базовый объём для открытия стартовой пары Buy/Sell.
MinLotsMultiplier 3 Количество минимальных лотов, добавляемых к объёму убыточной сделки при доливке.
StopLossPips 50 Размер стоп-лосса в пипсах. Ноль отключает стоп и трейлинг.
TrailingStopPips 5 Расстояние трейлинг-стопа в пипсах. Ноль — трейлинг не используется.
TrailingStepPips 5 Дополнительный шаг в пипсах, который цена должна пройти, прежде чем стоп будет подтянут. Должен быть положительным при включённом трейлинге.
GridStepPips 50 Минимальная дистанция (в пипсах) между худшей позицией и текущей ценой для новой доливки.
MinNearestPips 3 Если открытая сделка находится ближе этого порога к цене последнего стопа, повторный вход пропускается.

Особенности реализации

  • Используется SubscribeLevel1() для отслеживания bid/ask и запуска логики на каждом тике.
  • Заявки регистрируются через высокоуровневый RegisterOrder, а фактические исполнения обрабатываются в OnOwnTradeReceived.
  • Внутри стратегии ведётся собственный список позиций для имитации хеджирования, так как портфель StockSharp работает с чистой позицией.
  • Стоп-лоссы и трейлинг реализованы вручную: при достижении порога отправляется рыночная заявка на закрытие без отдельных стоп-ордеров.

Рекомендации по использованию

  1. Назначьте ликвидный инструмент и портфель, убедитесь, что заданы PriceStep, VolumeStep, MinVolume и MaxVolume для корректных расчётов.
  2. После запуска стратегия сразу создаст хедж и далее будет реагировать на события стопов по логике исходного советника.
  3. Подбирайте параметры в пипсах под волатильность инструмента: увеличение Grid Step снижает частоту доливок, а большой Min Lots Multiplier ускоряет рост объёмов.
  4. Контролируйте общий риск: мартингейл способен очень быстро наращивать позицию при серии убыточных стопов.

Python-версия намеренно не добавлена в эту папку согласно требованиям задачи.

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>
/// Urdala Trol strategy (simplified). Uses EMA with trailing stop logic
/// for grid-style entries based on trend direction.
/// </summary>
public class UrdalaTrolStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<decimal> _trailingPercent;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

	public decimal TrailingPercent
	{
		get => _trailingPercent.Value;
		set => _trailingPercent.Value = value;
	}

	public UrdalaTrolStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candles", "General");

		_emaLength = Param(nameof(EmaLength), 14)
			.SetGreaterThanZero()
			.SetDisplay("EMA Length", "EMA period", "Indicators");

		_trailingPercent = Param(nameof(TrailingPercent), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing %", "Trailing stop percent", "Risk");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var ema = new ExponentialMovingAverage { Length = EmaLength };

		decimal highSinceEntry = 0;
		decimal lowSinceEntry = decimal.MaxValue;
		decimal prevClose = 0;
		decimal prevEma = 0;
		var hasPrev = false;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ema, (ICandleMessage candle, decimal emaVal) =>
			{
				if (candle.State != CandleStates.Finished)
					return;

				if (!hasPrev)
				{
					prevClose = candle.ClosePrice;
					prevEma = emaVal;
					hasPrev = true;
					return;
				}

				if (!IsFormedAndOnlineAndAllowTrading())
				{
					prevClose = candle.ClosePrice;
					prevEma = emaVal;
					return;
				}

				var close = candle.ClosePrice;
				var high = candle.HighPrice;
				var low = candle.LowPrice;

				// Track trailing stop levels
				if (Position > 0)
				{
					if (high > highSinceEntry)
						highSinceEntry = high;

					var trailStop = highSinceEntry * (1m - TrailingPercent / 100m);
					if (close < trailStop)
					{
						SellMarket();
						highSinceEntry = 0;
						lowSinceEntry = decimal.MaxValue;
						return;
					}
				}
				else if (Position < 0)
				{
					if (low < lowSinceEntry)
						lowSinceEntry = low;

					var trailStop = lowSinceEntry * (1m + TrailingPercent / 100m);
					if (close > trailStop)
					{
						BuyMarket();
						highSinceEntry = 0;
						lowSinceEntry = decimal.MaxValue;
						return;
					}
				}

				// Entry signals based on EMA
				var bullishCross = prevClose <= prevEma && close > emaVal;
				var bearishCross = prevClose >= prevEma && close < emaVal;

				if (bullishCross && Position <= 0)
				{
					BuyMarket();
					highSinceEntry = high;
					lowSinceEntry = decimal.MaxValue;
				}
				else if (bearishCross && Position >= 0)
				{
					SellMarket();
					lowSinceEntry = low;
					highSinceEntry = 0;
				}

				prevClose = close;
				prevEma = emaVal;
			})
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ema);
			DrawOwnTrades(area);
		}
	}
}