Ver no GitHub

Estratégia de Rompimento de Intervalo Big Dog

A estratégia Big Dog procura uma janela de consolidação estreita dentro da sessão matinal de Londres e opera rompimentos dessa caixa. O consultor especializado MQL original colocava ordens stop quando o intervalo de preços entre as StartHour e StopHour especificadas permanecia dentro de um número configurável de pontos. O port do StockSharp mantém a mesma ideia e usa ordens de mercado quando ocorre o rompimento, acompanhadas de níveis dinâmicos de stop-loss e take-profit derivados dos extremos da consolidação.

Lógica de trading

  1. Coletar velas terminadas entre StartHour (inclusive) e StopHour (exclusivo por padrão) para construir o intervalo diário.
  2. Ignorar a sessão se a diferença entre o máximo e mínimo da sessão exceder MaxRangePoints (convertido em unidades de preço usando o tamanho de ponto ajustado).
  3. Após o fechamento da sessão, verificar a distância entre o melhor ask/bid atual e os níveis de rompimento. Um setup é ativado apenas se o mercado estiver pelo menos DistancePoints afastado do máximo (para entradas compradas) ou do mínimo (para entradas vendidas).
  4. Quando o preço rompe através do máximo ou mínimo preparado em uma vela subsequente, entrar com uma ordem de mercado dimensionada por OrderVolume (compensando automaticamente qualquer posição contrária).
  5. Atribuir imediatamente as saídas:
    • Operações compradas usam um stop-loss no mínimo da sessão registrado e um take-profit colocado TakeProfitPoints acima do nível de entrada.
    • Operações vendidas usam um stop-loss no máximo da sessão registrado e um take-profit colocado TakeProfitPoints abaixo do nível de entrada.
  6. Em cada vela terminada, a estratégia monitora o máximo/mínimo para decidir se o stop-loss ou take-profit foi atingido e fecha a posição adequadamente.
  7. No início de um novo dia de trading, todos os níveis em cache são reiniciados para evitar ordens remanescentes da sessão anterior.

Pontos ajustados. A estratégia converte entradas baseadas em pontos em distâncias de preço reais multiplicando-as pelo PriceStep do instrumento. Quando o ativo tem 3 ou 5 casas decimais, o valor é adicionalmente escalado por 10 para imitar a lógica de pip usada no EA original.

Parâmetros

Parâmetro Descrição Padrão
StartHour Hora do dia (0-23) quando a janela de consolidação começa. 14
StopHour Hora do dia (0-23) quando a janela de consolidação termina. 16
MaxRangePoints Altura máxima da caixa de sessão medida em pontos ajustados. 50
TakeProfitPoints Distância de take-profit em pontos ajustados a partir do preço de rompimento. 50
DistancePoints Distância mínima entre o preço atual e o nível de rompimento antes de ativar ordens. 20
OrderVolume Volume de cada operação de rompimento (também aplicado ao Volume da estratégia). 1
CandleType Tipo de vela usado para construir a caixa de sessão. Período de uma hora por padrão. 1h

Notas de implementação

  • A estratégia assina tanto velas quanto o livro de ordens. Os melhores valores de bid/ask são usados para avaliar os filtros de distância, recorrendo ao último fechamento de vela se não houver profundidade disponível.
  • As entradas são executadas com ordens de mercado. Isso reflete o comportamento das ordens stop pendentes originais enquanto permanece dentro da API de alto nível.
  • As decisões de stop-loss e take-profit são realizadas nos fechamentos de velas com base nos máximos e mínimos intrabarra, o que emula os níveis protetores da versão MQL sem registrar ordens filho adicionais.
  • O gerenciamento de estado diário cancela quaisquer ordens ativas e reinicia os máximos/mínimos em cache quando a data do calendário muda.
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>
/// Big Dog range breakout strategy that trades breakouts from a tight range built between configurable hours.
/// </summary>
public class BigDogStrategy : Strategy
{
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _stopHour;
	private readonly StrategyParam<decimal> _maxRangePoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _distancePoints;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _rangeHigh;
	private decimal? _rangeLow;
	private DateTime? _rangeDate;
	private bool _longReady;
	private bool _shortReady;
	private decimal? _longStopPrice;
	private decimal? _longTakeProfitPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfitPrice;
	private decimal _longEntryPrice;
	private decimal _shortEntryPrice;
	private decimal? _bestBid;
	private decimal? _bestAsk;
	private decimal _adjustedPointSize;

	/// <summary>
	/// Hour (0-23) when the consolidation range calculation starts.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour (0-23) when the consolidation range calculation stops.
	/// </summary>
	public int StopHour
	{
		get => _stopHour.Value;
		set => _stopHour.Value = value;
	}

	/// <summary>
	/// Maximum acceptable range height measured in adjusted points.
	/// </summary>
	public decimal MaxRangePoints
	{
		get => _maxRangePoints.Value;
		set => _maxRangePoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance measured in adjusted points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimum distance required between the current price and breakout level, measured in adjusted points.
	/// </summary>
	public decimal DistancePoints
	{
		get => _distancePoints.Value;
		set => _distancePoints.Value = value;
	}

	/// <summary>
	/// Order volume that will be used for entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for range calculation and breakout detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="BigDogStrategy"/> class.
	/// </summary>
	public BigDogStrategy()
	{
		_startHour = Param(nameof(StartHour), 2)
			.SetRange(0, 23)
			.SetDisplay("Start Hour", "Hour to begin measuring the range", "Session");

		_stopHour = Param(nameof(StopHour), 8)
			.SetRange(0, 23)
			.SetDisplay("Stop Hour", "Hour to stop measuring the range", "Session");

		_maxRangePoints = Param(nameof(MaxRangePoints), 50000m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range", "Maximum allowed height of the consolidation range (points)", "Trading");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit", "Take-profit distance in adjusted points", "Trading");

		_distancePoints = Param(nameof(DistancePoints), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Min Distance", "Minimum distance from current price to breakout level (points)", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume used for each breakout order", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candles timeframe used for range detection", "Data");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rangeHigh = null;
		_rangeLow = null;
		_rangeDate = null;
		_longReady = false;
		_shortReady = false;
		_longStopPrice = null;
		_longTakeProfitPrice = null;
		_shortStopPrice = null;
		_shortTakeProfitPrice = null;
		_longEntryPrice = 0m;
		_shortEntryPrice = 0m;
		_bestBid = null;
		_bestAsk = null;
		_adjustedPointSize = 0m;
	}

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

		Volume = OrderVolume;
		_adjustedPointSize = CalculateAdjustedPointSize();

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

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

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

		var currentDate = candle.OpenTime.Date;

		if (_rangeDate != currentDate)
		{
			ResetDailyState(currentDate);
		}

		UpdateRange(candle);

		if (candle.OpenTime.Hour >= StopHour)
		{
			PrepareBreakoutLevels(candle);
		}

		ProcessEntries(candle);
		ProcessRiskManagement(candle);
	}

	private void ResetDailyState(DateTime date)
	{
		_rangeDate = date;
		_rangeHigh = null;
		_rangeLow = null;
		_longReady = false;
		_shortReady = false;
		_longStopPrice = null;
		_longTakeProfitPrice = null;
		_shortStopPrice = null;
		_shortTakeProfitPrice = null;
	}

	private void UpdateRange(ICandleMessage candle)
	{
		var hour = candle.OpenTime.Hour;

		if (hour < StartHour || hour >= StopHour)
			return;

		_rangeHigh = _rangeHigh.HasValue
			? Math.Max(_rangeHigh.Value, candle.HighPrice)
			: candle.HighPrice;

		_rangeLow = _rangeLow.HasValue
			? Math.Min(_rangeLow.Value, candle.LowPrice)
			: candle.LowPrice;
	}

	private void PrepareBreakoutLevels(ICandleMessage candle)
	{
		if (!_rangeHigh.HasValue || !_rangeLow.HasValue)
			return;

		var rangeHeight = _rangeHigh.Value - _rangeLow.Value;
		var maxRange = ConvertToPrice(MaxRangePoints);

		if (rangeHeight >= maxRange)
		{
			// Reset pending plans when the range becomes too wide.
			_longReady = false;
			_shortReady = false;
			return;
		}

		var minDistance = ConvertToPrice(DistancePoints);
		var ask = _bestAsk ?? candle.ClosePrice;
		var bid = _bestBid ?? candle.ClosePrice;

		if (!_longReady && Position >= 0 && (_rangeHigh.Value - ask) > minDistance)
		{
			_longReady = true;
			_longEntryPrice = _rangeHigh.Value;
			_longStopPrice = _rangeLow.Value;
			_longTakeProfitPrice = _rangeHigh.Value + ConvertToPrice(TakeProfitPoints);
		}

		if (!_shortReady && Position <= 0 && (bid - _rangeLow.Value) > minDistance)
		{
			_shortReady = true;
			_shortEntryPrice = _rangeLow.Value;
			_shortStopPrice = _rangeHigh.Value;
			_shortTakeProfitPrice = _rangeLow.Value - ConvertToPrice(TakeProfitPoints);
		}
	}

	private void ProcessEntries(ICandleMessage candle)
	{
		var volume = OrderVolume;

		if (_longReady && candle.HighPrice >= _longEntryPrice && Position <= 0)
		{
			// Enter long on breakout of the session high.
			BuyMarket();

			_longReady = false;
			_shortReady = false;
		}

		if (_shortReady && candle.LowPrice <= _shortEntryPrice && Position >= 0)
		{
			// Enter short on breakout of the session low.
			SellMarket();

			_shortReady = false;
			_longReady = false;
		}
	}

	private void ProcessRiskManagement(ICandleMessage candle)
	{
		if (Position > 0 && _longStopPrice.HasValue && _longTakeProfitPrice.HasValue)
		{
			// Close the long position if stop-loss or take-profit levels are touched.
			if (candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				_longStopPrice = null;
				_longTakeProfitPrice = null;
			}
			else if (candle.HighPrice >= _longTakeProfitPrice.Value)
			{
				SellMarket();
				_longStopPrice = null;
				_longTakeProfitPrice = null;
			}
		}
		else if (Position < 0 && _shortStopPrice.HasValue && _shortTakeProfitPrice.HasValue)
		{
			// Close the short position if stop-loss or take-profit levels are touched.
			if (candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				_shortStopPrice = null;
				_shortTakeProfitPrice = null;
			}
			else if (candle.LowPrice <= _shortTakeProfitPrice.Value)
			{
				BuyMarket();
				_shortStopPrice = null;
				_shortTakeProfitPrice = null;
			}
		}
		else
		{
			_longStopPrice = null;
			_longTakeProfitPrice = null;
			_shortStopPrice = null;
			_shortTakeProfitPrice = null;
		}
	}

	private decimal ConvertToPrice(decimal points)
	{
		return points * _adjustedPointSize;
	}

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

		var decimals = Security?.Decimals ?? 0;
		var multiplier = decimals == 3 || decimals == 5 ? 10m : 1m;

		return step * multiplier;
	}
}