Ver en GitHub

Estrategia de Rompimiento de Rango Big Dog

La estrategia Big Dog busca una ventana de consolidación estrecha dentro de la sesión matutina de Londres y opera rompimientos desde esa caja. El asesor experto MQL original colocaba órdenes stop una vez que el rango de precios entre las horas StartHour y StopHour especificadas permanecía dentro de un número configurable de puntos. El port de StockSharp mantiene la misma idea y usa órdenes de mercado cuando ocurre el rompimiento, acompañadas de niveles dinámicos de stop-loss y take-profit derivados de los extremos de la consolidación.

Lógica de trading

  1. Recopilar velas terminadas entre StartHour (inclusive) y StopHour (exclusivo por defecto) para construir el rango diario.
  2. Ignorar la sesión si la diferencia entre el máximo y mínimo de la sesión supera MaxRangePoints (convertido en unidades de precio usando el tamaño de punto ajustado).
  3. Después de que la sesión cierre, verificar la distancia entre el mejor ask/bid actual y los niveles de rompimiento. Un setup se activa solo si el mercado está al menos a DistancePoints del máximo (para entradas largas) o del mínimo (para entradas cortas).
  4. Cuando el precio rompe a través del máximo o mínimo preparado en una vela subsiguiente, entrar con una orden de mercado dimensionada por OrderVolume (compensando automáticamente cualquier posición contraria).
  5. Asignar inmediatamente las salidas:
    • Las operaciones largas usan un stop-loss en el mínimo de la sesión registrado y un take-profit colocado TakeProfitPoints por encima del nivel de entrada.
    • Las operaciones cortas usan un stop-loss en el máximo de la sesión registrado y un take-profit colocado TakeProfitPoints por debajo del nivel de entrada.
  6. En cada vela terminada la estrategia monitorea el máximo/mínimo para decidir si el stop-loss o take-profit fue alcanzado y cierra la posición en consecuencia.
  7. Al comienzo de un nuevo día de trading, todos los niveles en caché se reinician para evitar órdenes sobrantes de la sesión anterior.

Puntos ajustados. La estrategia convierte las entradas basadas en puntos en distancias de precio reales multiplicándolas por el PriceStep del instrumento. Cuando el activo tiene 3 o 5 decimales, el valor se escala adicionalmente por 10 para imitar la lógica pip usada en el EA original.

Parámetros

Parámetro Descripción Predeterminado
StartHour Hora del día (0-23) cuando comienza la ventana de consolidación. 14
StopHour Hora del día (0-23) cuando termina la ventana de consolidación. 16
MaxRangePoints Altura máxima de la caja de sesión medida en puntos ajustados. 50
TakeProfitPoints Distancia de take-profit en puntos ajustados desde el precio de rompimiento. 50
DistancePoints Distancia mínima entre el precio actual y el nivel de rompimiento antes de activar órdenes. 20
OrderVolume Volumen de cada operación de rompimiento (también se aplica al Volume de la estrategia). 1
CandleType Tipo de vela usado para construir la caja de sesión. Marco temporal de una hora por defecto. 1h

Notas de implementación

  • La estrategia se suscribe tanto a velas como al libro de órdenes. Los mejores valores de bid/ask se usan para evaluar los filtros de distancia, recurriendo al último cierre de vela si no hay profundidad disponible.
  • Las entradas se ejecutan con órdenes de mercado. Esto refleja el comportamiento de las órdenes stop pendientes originales mientras se mantiene dentro de la API de alto nivel.
  • Las decisiones de stop-loss y take-profit se realizan en los cierres de velas basándose en los máximos y mínimos intrabarra, lo que emula los niveles protectores de la versión MQL sin registrar órdenes hijo adicionales.
  • La gestión del estado diario cancela cualquier orden activa y reinicia los máximos/mínimos en caché cuando cambia la fecha calendario.
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;
	}
}