Открыть на GitHub

Стратегия Daily BreakPoint

Обзор

Daily BreakPoint Strategy — это портирование эксперта MetaTrader 5 «Daily BreakPoint» (сборка 19498) в экосистему StockSharp. Алгоритм отслеживает расстояние между текущей ценой и дневным открытием. Когда движение от открытия превышает заданный порог, а тело последней свечи укладывается в допустимый диапазон, стратегия либо открывает позицию по направлению пробоя, либо разворачивает существующую позицию в зависимости от флага CloseBySignal.

Стратегия одновременно использует два типа данных:

  1. Внутридневные свечи (параметр CandleType) для генерации торговых сигналов.
  2. Дневные свечи для отслеживания актуального открытия торговой сессии.

Логика торговли

  1. После закрытия каждой внутридневной свечи стратегия получает цену открытия текущего дня и вычисляет уровни пробоя на основе BreakPointPips (значение переводится из пунктов в абсолютную цену через шаг цены инструмента).
  2. Размер тела последней свечи должен лежать в диапазоне [LastBarSizeMinPips, LastBarSizeMaxPips].
  3. Бычий сценарий
    • Свеча закрывается выше открытия (Close > Open).
    • Цена закрытия как минимум на BreakPointPips выше дневного открытия.
    • Уровень пробоя (дневное открытие + порог) находится внутри тела свечи.
    • Если CloseBySignal = false, открывается длинная позиция. Если CloseBySignal = true, текущие лонги закрываются и открывается короткая позиция.
  4. Медвежий сценарий симметричен: свеча закрывается ниже открытия, расстояние до дневного открытия больше BreakPointPips, уровень пробоя лежит внутри тела — в результате открывается шорт (CloseBySignal = false) или происходит разворот в лонг (CloseBySignal = true).
  5. Заявки отправляются по рынку с объемом OrderVolume. Объём позиции суммируется, поэтому повторные сигналы могут наращивать позицию.

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

  • Стоп-лосс / тейк-профит: задаются в пунктах (StopLossPips, TakeProfitPips). Нулевое значение отключает соответствующий уровень. Проверка на срабатывание выполняется по максимумам и минимумам свечей.
  • Трейлинг-стоп: активируется, если TrailingStopPips > 0. После того как прибыль превышает TrailingStopPips + TrailingStepPips, стоп подтягивается на расстояние TrailingStopPips. Параметр шага предотвращает излишне частые перестановки в боковом рынке.
  • Все расстояния в пунктах переводятся в абсолютные цены через PriceStep. Для инструментов с 3 или 5 знаками после запятой размер пункта равен десяти шагам цены — как и в оригинальном советнике.

Параметры

Параметр Описание
OrderVolume Базовый объем каждой рыночной заявки.
CloseBySignal При true стратегия закрывает позиции и открывает противоположную при появлении сигнала.
BreakPointPips Расстояние от дневного открытия, необходимое для подтверждения пробоя.
LastBarSizeMinPips / LastBarSizeMaxPips Допустимый диапазон размера тела сигнальной свечи.
TrailingStopPips Дистанция трейлинг-стопа. 0 — отключить.
TrailingStepPips Минимальное дополнительное движение перед каждым смещением стопа.
StopLossPips Фиксированный стоп-лосс. 0 — без стопа.
TakeProfitPips Фиксированный тейк-профит. 0 — без тейка.
CandleType Тип внутридневных свечей для расчетов.

Примечания по использованию

  • Стратегия автоматически подписывается на внутридневные и дневные свечи. Убедитесь, что поставщик данных поддерживает необходимые таймфреймы.
  • Сигналы анализируются только после закрытия свечи, поэтому заявки отправляются по цене закрытия сигнального бара.
  • Конвертация пунктов рассчитана на форекс-котировки. Для инструментов с нестандартным шагом цены скорректируйте значения параметров.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Daily breakout strategy that reacts to the distance from the daily open.
/// Converted from the MetaTrader Daily BreakPoint expert advisor.
/// </summary>
public class DailyBreakPointStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _closeBySignal;
	private readonly StrategyParam<decimal> _breakPointPips;
	private readonly StrategyParam<decimal> _lastBarSizeMinPips;
	private readonly StrategyParam<decimal> _lastBarSizeMaxPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _currentDayOpen;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal _pipSize;

	/// <summary>
	/// Order volume.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Reverse the position when the opposite signal appears.
	/// </summary>
	public bool CloseBySignal
	{
		get => _closeBySignal.Value;
		set => _closeBySignal.Value = value;
	}

	/// <summary>
	/// Break distance from the daily open expressed in pips.
	/// </summary>
	public decimal BreakPointPips
	{
		get => _breakPointPips.Value;
		set => _breakPointPips.Value = value;
	}

	/// <summary>
	/// Minimum size of the previous bar body in pips.
	/// </summary>
	public decimal LastBarSizeMinPips
	{
		get => _lastBarSizeMinPips.Value;
		set => _lastBarSizeMinPips.Value = value;
	}

	/// <summary>
	/// Maximum size of the previous bar body in pips.
	/// </summary>
	public decimal LastBarSizeMaxPips
	{
		get => _lastBarSizeMaxPips.Value;
		set => _lastBarSizeMaxPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing stop step in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Fixed stop loss in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Fixed take profit in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Intraday candle type used for signal calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="DailyBreakPointStrategy"/> class.
	/// </summary>
	public DailyBreakPointStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Default order volume", "General");

		_closeBySignal = Param(nameof(CloseBySignal), true)
		.SetDisplay("Close By Signal", "Reverse existing position on opposite signal", "General");

		_breakPointPips = Param(nameof(BreakPointPips), 5m)
		.SetGreaterThanZero()
		.SetDisplay("Break Point (pips)", "Distance from the daily open", "Signals");

		_lastBarSizeMinPips = Param(nameof(LastBarSizeMinPips), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Last Bar Min (pips)", "Minimum body size of the previous bar", "Signals");

		_lastBarSizeMaxPips = Param(nameof(LastBarSizeMaxPips), 5000m)
		.SetGreaterThanZero()
		.SetDisplay("Last Bar Max (pips)", "Maximum body size of the previous bar", "Signals");

		_trailingStopPips = Param(nameof(TrailingStopPips), 2m)
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 2m)
		.SetDisplay("Trailing Step (pips)", "Minimum move before trailing", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetDisplay("Stop Loss (pips)", "Fixed stop loss distance", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 30m)
		.SetDisplay("Take Profit (pips)", "Fixed take profit distance", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Intraday candle series", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType), (Security, TimeSpan.FromMinutes(5).TimeFrame())];
	}

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

		_currentDayOpen = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_pipSize = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = OrderVolume;
		_pipSize = CalculatePipSize();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent)
		);

		var intradaySubscription = SubscribeCandles(CandleType);
		intradaySubscription.Bind(ProcessCandle).Start();

		var dailySubscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		dailySubscription.Bind(ProcessDailyCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, intradaySubscription);
			DrawOwnTrades(area);
		}
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0.0001m;
		if (step <= 0m)
		step = 0.0001m;

		var decimals = Security?.Decimals;
		if (decimals == 3 || decimals == 5)
		return step * 10m;

		return step;
	}

	private decimal NormalizePrice(decimal price)
	{
		var step = Security?.PriceStep;
		if (step is null || step.Value <= 0m)
		return price;

		var value = price / step.Value;
		var rounded = Math.Round(value, 0, MidpointRounding.AwayFromZero);
		return rounded * step.Value;
	}

	private void ProcessDailyCandle(ICandleMessage candle)
	{
		if (candle.State == CandleStates.Finished || candle.State == CandleStates.Active)
		_currentDayOpen = candle.OpenPrice;
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		Volume = OrderVolume;


		if (_pipSize <= 0m)
		_pipSize = CalculatePipSize();

		var dayOpen = _currentDayOpen;
		if (dayOpen is null)
		return;

		var breakOffset = BreakPointPips * _pipSize;
		var minBody = LastBarSizeMinPips * _pipSize;
		var maxBody = LastBarSizeMaxPips * _pipSize;
		var trailingStop = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;
		var stopLossOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
		var takeProfitOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;

		UpdateTrailing(candle, trailingStop, trailingStep);
		HandleRiskExits(candle);

		var bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		var minPrice = Math.Min(candle.OpenPrice, candle.ClosePrice);
		var maxPrice = Math.Max(candle.OpenPrice, candle.ClosePrice);

		var breakBuy = dayOpen.Value + breakOffset;
		var breakSell = dayOpen.Value - breakOffset;

		var bullishBody = candle.ClosePrice > candle.OpenPrice;
		var bearishBody = candle.ClosePrice < candle.OpenPrice;

		var bullishSignal = bullishBody && breakOffset > 0m &&
		candle.ClosePrice - dayOpen.Value >= breakOffset &&
		bodySize >= minBody &&
		(maxBody <= 0m || bodySize <= maxBody);

		var bearishSignal = bearishBody && breakOffset > 0m &&
		dayOpen.Value - candle.ClosePrice >= breakOffset &&
		bodySize >= minBody &&
		(maxBody <= 0m || bodySize <= maxBody);

		if (bullishSignal)
		{
			ExecuteBullishSignal(candle.ClosePrice, stopLossOffset, takeProfitOffset);
		}
		else if (bearishSignal)
		{
			ExecuteBearishSignal(candle.ClosePrice, stopLossOffset, takeProfitOffset);
		}
	}

	private void UpdateTrailing(ICandleMessage candle, decimal trailingStop, decimal trailingStep)
	{
		if (trailingStop <= 0m)
		return;

		if (Position > 0 && _longEntryPrice.HasValue)
		{
			var profit = candle.ClosePrice - _longEntryPrice.Value;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = candle.ClosePrice - (trailingStop + trailingStep);
				if (!_longStopPrice.HasValue || _longStopPrice.Value < threshold)
				_longStopPrice = NormalizePrice(candle.ClosePrice - trailingStop);
			}
		}
		else if (Position < 0 && _shortEntryPrice.HasValue)
		{
			var profit = _shortEntryPrice.Value - candle.ClosePrice;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = candle.ClosePrice + (trailingStop + trailingStep);
				if (!_shortStopPrice.HasValue || _shortStopPrice.Value > threshold || _shortStopPrice.Value == 0m)
				_shortStopPrice = NormalizePrice(candle.ClosePrice + trailingStop);
			}
		}
	}

	private void HandleRiskExits(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0m && _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				ResetLongState();
				return;
			}

			if (volume > 0m && _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value)
			{
				SellMarket();
				ResetLongState();
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0m && _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				ResetShortState();
				return;
			}

			if (volume > 0m && _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value)
			{
				BuyMarket();
				ResetShortState();
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}
	}

	private void ExecuteBullishSignal(decimal entryPrice, decimal stopLossOffset, decimal takeProfitOffset)
	{
		if (CloseBySignal)
		{
			if (Position > 0)
			{
				var volume = Math.Abs(Position);
				SellMarket();
			}

			ResetLongState();

			SellMarket();

			_shortEntryPrice = entryPrice;
			_shortStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice + stopLossOffset) : null;
			_shortTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice - takeProfitOffset) : null;
		}
		else
		{
			BuyMarket();

			_longEntryPrice = entryPrice;
			_longStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice - stopLossOffset) : null;
			_longTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice + takeProfitOffset) : null;
			ResetShortState();
		}
	}

	private void ExecuteBearishSignal(decimal entryPrice, decimal stopLossOffset, decimal takeProfitOffset)
	{
		if (CloseBySignal)
		{
			if (Position < 0)
			{
				var volume = Math.Abs(Position);
				BuyMarket();
			}

			ResetShortState();

			BuyMarket();

			_longEntryPrice = entryPrice;
			_longStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice - stopLossOffset) : null;
			_longTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice + takeProfitOffset) : null;
		}
		else
		{
			SellMarket();

			_shortEntryPrice = entryPrice;
			_shortStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice + stopLossOffset) : null;
			_shortTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice - takeProfitOffset) : null;
			ResetLongState();
		}
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
	}
}