Открыть на GitHub

Стратегия Stop Hunter

Общая информация

  • Перенос советника MetaTrader 4 Stop Hunter на высокоуровневый API StockSharp.
  • Работает с круглыми ценами: вычисляет уровни, у которых последние Zeroes знаков после запятой равны нулю, и размещает стоп-заявки внутри этих зон.
  • Сохраняет оригинальную идею скрытых тейк-профитов и стоп-лоссов, контролируя выходы самостоятельно.
  • Реализует двухфазное сопровождение позиции: половина объёма фиксируется на первом таргете, остаток ведётся до удвоенного расстояния.

Источник данных и подписки

  1. В методе OnStarted оформляется подписка на Level1 (SubscribeLevel1().Bind(ProcessLevel1)), т.е. достаточно потока лучших Bid/Ask.
  2. Каждое обновление сохраняет последние значения и запускает расчёт только после проверки IsFormedAndOnlineAndAllowTrading().
  3. При наличии визуализации создаётся область графика и выводятся собственные сделки (DrawOwnTrades).

Логика постановки заявок

  • Поиск круглых уровней
    • Используется шаг цены инструмента (Security.PriceStep) как аналог Point из MQL.
    • Вычисляется длина шага круга: roundStep = PriceStep * 10^Zeroes.
    • Находится следующий круглый уровень выше Bid (Math.Ceiling(bid / roundStep) * roundStep).
    • Если Ask уже находится внутри буфера, уровень сдвигается вперёд, чтобы заявка не стояла вплотную к спреду.
    • Нижний уровень (LevelS) рассчитывается на один круговой шаг ниже и корректируется аналогично.
  • Выставление стоп-заявок
    • При активной торговле в лонг формируется BuyStop на LevelB - DistancePoints * PriceStep, если нет действующей заявки и открытых шортов.
    • В шорт заявка SellStop размещается на LevelS + DistancePoints * PriceStep, если длинных позиций нет.
    • Старые заявки снимаются, когда круглый уровень смещается или цена уходит дальше чем на roundStep + DistancePoints * 50 * PriceStep, что повторяет логику оригинального цикла Delete*Stop().
    • Общее количество «слотов» (позиция + заявки) ограничено суммой MaxLongPositions + MaxShortPositions.

Управление виртуальными выходами

  • Отслеживаются средняя цена входа (Position.AveragePrice) и текущий объём позиции.
  • Два счётчика (_takeProfitExtension, _stopLossExtension) накапливают смещения тейк-профита и стоп-лосса:
    • Первый таргет закрывает половину объёма при движении цены на TakeProfitPoints * PriceStep в нужную сторону.
    • После частичного выхода дистанции увеличиваются ещё на TakeProfitPoints/StopLossPoints, активируя режим "SecondTrade".
    • Оставшийся объём закрывается либо при достижении удвоенного тейка, либо при срабатывании удвоенного стопа.
  • Закрытие выполняется рыночными командами (SellMarket / BuyMarket), как и в MQL-версии.
  • При появлении позиции противоположная стоп-заявка отменяется, чтобы не образовывать хедж.

Управление капиталом

  • Функция Call_MM() перенесена напрямую: объём = Баланс / 100000 * RiskPercent.
  • Полученное значение ограничивается интервалом [MinimumVolume, MaximumVolume] и округляется к шагу объёма инструмента (либо до 2/1/0 знаков после запятой, если шаг не задан).
  • Для частичных выходов берётся половина текущего объёма с учётом шагов.

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

  • Используется только высокоуровневый API StockSharp: работа с уровнем 1, методы BuyStop, SellStop, BuyMarket, SellMarket.
  • Все внутренние состояния сбрасываются через ResetState() при старте и сбросе стратегии.
  • Логика OnOwnTradeReceived дожидается подтверждения сделки, прежде чем обновлять флаг _secondTrade, аналогично MQL-проверке OrderClose.
  • OnOrderChanged очищает ссылки на заявки при отмене или ошибке, предотвращая повторное использование.

Отличия от версии MetaTrader

  • В StockSharp используется неттинг: допускается только одна совокупная позиция. Значения MaxLongPositions/MaxShortPositions оставлены равными 1, что соответствует штатной работе советника.
  • Вместо AccountFreeMargin применяется Portfolio.CurrentValue (с запасным вариантом BeginValue), что удобнее при тестах на фондовом рынке.
  • После закрытия позиции счётчики тейка и стопа обнуляются, что исключает накопление смещений, присутствовавшее в старом коде EA.
  • Все комментарии в коде — на английском языке, документация приведена на трёх языках согласно правилам репозитория.

Параметры

Параметр Значение по умолчанию Описание
Zeroes 2 Количество нулевых знаков справа, определяющих «круглый» уровень.
DistancePoints 15 Смещение (в пунктах) между круглой ценой и стоп-заявкой.
TakeProfitPoints 15 Размер скрытого тейк-профита в пунктах. После частичного выхода используется повторно.
StopLossPoints 15 Скрытый стоп-лосс в пунктах (удваивается на втором этапе сопровождения).
EnableLongOrders true Разрешение на постановку BuyStop.
EnableShortOrders true Разрешение на постановку SellStop.
RiskPercent 5 Процент капитала, преобразуемый в объём заявки.
MinimumVolume 0.1 Минимально допустимый объём после округления.
MaximumVolume 30 Максимальный объём заявки.
MaxLongPositions 1 Допустимое число длинных «слотов» (позиция + отложенные заявки).
MaxShortPositions 1 Допустимое число коротких «слотов».

Рекомендации по применению

  1. Выбирайте инструмент, шаг цены которого соответствует логике MQL Point (для большинства форекс-пар достаточно Zeroes = 2).
  2. Уточняйте шаг объёма на бирже/у брокера и настраивайте MinimumVolume, чтобы избежать ошибок Invalid volume.
  3. Помните, что стопы виртуальные: стратегия должна быть постоянно подключена, иначе защитные уровни не будут выполнены.
  4. При необходимости дополните работу стратегией StartProtection() для биржевых стоп-заявок.
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Stop Hunter: EMA crossover with ATR trailing stops.
/// </summary>
public class StopHunterStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private decimal _bestPrice;

	public StopHunterStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");
		_fastEmaLength = Param(nameof(FastEmaLength), 10)
			.SetDisplay("Fast EMA Length", "Fast EMA period.", "Indicators");
		_slowEmaLength = Param(nameof(SlowEmaLength), 30)
			.SetDisplay("Slow EMA Length", "Slow EMA period.", "Indicators");
		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int FastEmaLength { get => _fastEmaLength.Value; set => _fastEmaLength.Value = value; }
	public int SlowEmaLength { get => _slowEmaLength.Value; set => _slowEmaLength.Value = value; }
	public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _bestPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _bestPrice = 0;
		var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
		var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(fastEma, slowEma, atr, ProcessCandle).Start();
		var area = CreateChartArea();
		if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, fastEma); DrawIndicator(area, slowEma); DrawOwnTrades(area); }
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished) return;
		if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0) { _prevFast = fastVal; _prevSlow = slowVal; return; }
		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (close > _bestPrice) _bestPrice = close;
			if (close <= _bestPrice - atrVal * 2m || (fastVal < slowVal && _prevFast >= _prevSlow)) { SellMarket(); _entryPrice = 0; _bestPrice = 0; }
		}
		else if (Position < 0)
		{
			if (close < _bestPrice) _bestPrice = close;
			if (close >= _bestPrice + atrVal * 2m || (fastVal > slowVal && _prevFast <= _prevSlow)) { BuyMarket(); _entryPrice = 0; _bestPrice = 0; }
		}

		if (Position == 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow) { _entryPrice = close; _bestPrice = close; BuyMarket(); }
			else if (fastVal < slowVal && _prevFast >= _prevSlow) { _entryPrice = close; _bestPrice = close; SellMarket(); }
		}
		_prevFast = fastVal; _prevSlow = slowVal;
	}
}