Ver en GitHub

Previous Day High Low Long

Solo largo: cruce ascendente del high o low observado del día UTC anterior, con ADX nativo creciente dentro de una sesión UTC. Sale por objetivo desde ejecución real, stop dinámico o fin de sesión.

Niveles y entrada

Solo velas terminadas, cinco minutos por defecto. Agrupa high/low por fecha UTC de apertura y congela máximo/mínimo observados al comenzar la siguiente fecha. El día anterior debe empezar con una vela observada a medianoche y las fechas ser consecutivas; no se inventan niveles para un inicio parcial o fecha ausente. Velas actuales no amplían niveles anteriores.

Cruce high: previousClose <= previousDayHigh && close > previousDayHigh; low usa el nivel bajo anterior, también hacia arriba. ADX nativo AdxLength=11 debe estar formado y superar estrictamente su valor formado anterior, sin umbral adicional o filtro direccional DI.

Solo desde cero, compra a mercado Volume dentro de [SessionStart,SessionEnd) por hora UTC de envío, predeterminado09:30–16:00. Cada vela actualiza cierre, día y ADX: cruces rechazados no se aplazan. Un cruce nuevo permite reentrada tras cerrar, sin límite diario ni espera de seis horas. No abre cortos.

Protección y cierre

MaxProfit=150 y MaxStopLoss=15 son distancias absolutas de precio, no porcentajes ni ticks del instrumento. La compra real inicia entryPrice/highWater; compras parciales adicionales actualizan el precio medio ponderado por volumen, reducciones conservan la base y posición cero la reinicia.

Cada bid positivo ejecutable o close terminado eleva highWater al máximo del anterior y precio actual. Vende toda la posición larga real si precio≤highWater-MaxStopLoss o≥entryPrice+MaxProfit. Prioridad: fin de sesión, stop dinámico, objetivo. No convierte high de vela en cotización ejecutable.

Temporizador de tiempo de mercado de un minuto, bids y velas cierran en/después de SessionEnd o en la siguiente fecha UTC. Horas ordenadas dentro de un día;00:00–24:00 termina en la siguiente medianoche. Una orden pendiente impide duplicados; historia día/ADX sigue avanzando. El callback que cierra retorna sin entrar otra vez.

Monitoreo local, sin stop nativo de bolsa. Protección intravela por cotizaciones requiere Level1 bid; sin él quedan cierres. Ejecución puede diferir del umbral.

Parámetro Valor
MaxProfit / MaxStopLoss 150 / 15, distancias positivas absolutas
AdxLength 11, positivo
SessionStart / SessionEnd 09:30 / 16:00 UTC, inicio incluido y fin excluido
CandleType 5 minutos

Pruebas C#/Python agrupan días y calculan independientemente Wilder ATR/DI/DX/ADX en el archivo BTC/TON. Verifican cada entrada/salida real, ambos niveles, rechazos ADX, historia parcial inicial, ejecuciones, stops/objetivos por cotizaciones y cierre obligatorio. No demuestran rentabilidad.

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using Ecng.Common;

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

/// <summary>
/// Buys a previous UTC day's high/low upward close cross with rising ADX during a UTC session.
/// Actual-fill profit targets, observed-price trailing stops and session-end closes protect longs.
/// </summary>
public class PreviousDayHighLowLongStrategy : Strategy
{
	private readonly StrategyParam<decimal> _maxProfit;
	private readonly StrategyParam<decimal> _maxStopLoss;
	private readonly StrategyParam<int> _adxLength;
	private readonly StrategyParam<TimeSpan> _sessionStart;
	private readonly StrategyParam<TimeSpan> _sessionEnd;
	private readonly StrategyParam<DataType> _candleType;
	private DateTime? _day;
	private decimal _dayHigh;
	private decimal _dayLow;
	private bool _isDayStartObserved;
	private bool _isPreviousDayAvailable;
	private decimal _previousDayHigh;
	private decimal _previousDayLow;
	private decimal? _previousClose;
	private decimal? _previousAdx;
	private DateTime? _entryDate;
	private decimal _filledPosition;
	private decimal _entryPrice;
	private decimal _extreme;
	private Order _pendingOrder;

	/// <summary>Initializes profit150, trailing distance15 in price units, ADX11 and UTC09:30–16:00.</summary>
	public PreviousDayHighLowLongStrategy()
	{
		_maxProfit = Param(nameof(MaxProfit), 150m).SetGreaterThanZero()
			.SetDisplay("Maximum Profit", "Target distance above actual entry price, in absolute price units", "Protection");
		_maxStopLoss = Param(nameof(MaxStopLoss), 15m).SetGreaterThanZero()
			.SetDisplay("Maximum Stop Loss", "Trailing distance below highest observed price, in absolute price units", "Protection");
		_adxLength = Param(nameof(AdxLength), 11).SetGreaterThanZero()
			.SetDisplay("ADX Length", "Native directional-strength period; current ADX must rise", "Indicators");
		_sessionStart = Param(nameof(SessionStart), TimeSpan.FromMinutes(570))
			.SetDisplay("Session Start", "Inclusive UTC entry time", "Session");
		_sessionEnd = Param(nameof(SessionEnd), TimeSpan.FromHours(16))
			.SetDisplay("Session End", "Exclusive UTC entry time and mandatory close", "Session");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Finished candles supplying UTC day levels and ADX", "General");
		OrderRegistering += order => _pendingOrder = order;
		OwnTradeReceived += ObserveFill;
	}

	/// <summary>Positive profit target distance from actual entry price, in absolute price units.</summary>
	public decimal MaxProfit { get => _maxProfit.Value; set => _maxProfit.Value = value; }

	/// <summary>Positive trailing distance from highest observed price, in absolute price units.</summary>
	public decimal MaxStopLoss { get => _maxStopLoss.Value; set => _maxStopLoss.Value = value; }

	/// <summary>Positive native ADX period.</summary>
	public int AdxLength { get => _adxLength.Value; set => _adxLength.Value = value; }

	/// <summary>Inclusive UTC entry time, before SessionEnd.</summary>
	public TimeSpan SessionStart { get => _sessionStart.Value; set => _sessionStart.Value = value; }

	/// <summary>Exclusive UTC entry time and mandatory close, at most twenty-four hours.</summary>
	public TimeSpan SessionEnd { get => _sessionEnd.Value; set => _sessionEnd.Value = value; }

	/// <summary>Finished candle type; five minutes by default.</summary>
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	private void ResetState()
	{
		_day = null;
		_previousClose = _previousAdx = null;
		_entryDate = null;
		_dayHigh = _dayLow = _previousDayHigh = _previousDayLow = 0m;
		_isDayStartObserved = _isPreviousDayAvailable = false;
		_filledPosition = _entryPrice = _extreme = 0m;
		_pendingOrder = null;
	}

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

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		if (MaxProfit <= 0m || MaxStopLoss <= 0m || AdxLength <= 0 || SessionStart < TimeSpan.Zero
			|| SessionStart >= SessionEnd || SessionEnd > TimeSpan.FromDays(1))
			throw new ArgumentException("Risk distances and ADX period must be positive; session times must be ordered within a UTC day.");
		ResetState();
		var quotes = new Subscription(DataType.Level1, Security);
		quotes.MarketData.BuildField = Level1Fields.BestBidPrice;
		SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
		StartTimer(TimeSpan.FromMinutes(1), () => TrySessionExit());
		var adx = new AverageDirectionalIndex { Length = AdxLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.BindEx([adx], ProcessCandle, allowEmpty: true).Start();
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private bool IsPending => _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);

	private void Submit(Sides side, decimal volume, string comment)
		=> RegisterOrder(new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		});

	private bool TrySessionExit()
	{
		var isOver = _entryDate is DateTime date && (CurrentTime.Date > date || CurrentTime.TimeOfDay >= SessionEnd);
		if (!isOver || Position <= 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
			return false;
		Submit(Sides.Sell, Position, "Previous-day session close");
		return true;
	}

	private void ObserveFill(Subscription subscription, MyTrade trade)
	{
		var previous = _filledPosition;
		var volume = trade.Trade.Volume;
		_filledPosition += trade.Order.Side == Sides.Buy ? volume : -volume;
		if (trade.Order.Side == Sides.Buy && _filledPosition > 0m)
		{
			_entryPrice = (_entryPrice * previous + trade.Trade.Price * volume) / _filledPosition;
			_extreme = previous == 0m ? trade.Trade.Price : Math.Max(_extreme, trade.Trade.Price);
		}
		if (_filledPosition == 0m)
			_entryPrice = _extreme = 0m;
	}

	private void ObservePrice(decimal price)
	{
		if (TrySessionExit() || Position <= 0m || _entryPrice <= 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
			return;
		_extreme = Math.Max(_extreme, price);
		var comment = price <= _extreme - MaxStopLoss ? "Previous-day trailing stop"
			: price >= _entryPrice + MaxProfit ? "Previous-day profit target" : null;
		if (comment != null)
			Submit(Sides.Sell, Position, comment);
	}

	private void ObserveQuote(Level1ChangeMessage quote)
	{
		if (quote.TryGetDecimal(Level1Fields.BestBidPrice) is decimal price && price > 0m)
			ObservePrice(price);
	}

	private void ObserveDay(ICandleMessage candle)
	{
		var date = candle.OpenTime.Date;
		if (_day != date)
		{
			_isPreviousDayAvailable = _isDayStartObserved && _day == date.AddDays(-1);
			_previousDayHigh = _dayHigh;
			_previousDayLow = _dayLow;
			_day = date;
			_dayHigh = candle.HighPrice;
			_dayLow = candle.LowPrice;
			_isDayStartObserved = candle.OpenTime.TimeOfDay == TimeSpan.Zero;
		}
		else
		{
			_dayHigh = Math.Max(_dayHigh, candle.HighPrice);
			_dayLow = Math.Min(_dayLow, candle.LowPrice);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue[] values)
	{
		if (candle.State != CandleStates.Finished)
			return;
		var previousClose = _previousClose;
		_previousClose = candle.ClosePrice;
		ObserveDay(candle);
		var previousAdx = _previousAdx;
		var value = (IAverageDirectionalIndexValue)values[0];
		var currentAdx = value.IsFormed ? value.MovingAverage : null;
		if (currentAdx is not null)
			_previousAdx = currentAdx;
		if (Position > 0m)
		{
			ObservePrice(candle.ClosePrice);
			return;
		}
		if (Position != 0m || IsPending || !IsFormedAndOnlineAndAllowTrading() || !_isPreviousDayAvailable
			|| previousClose is not decimal close || previousAdx is not decimal prior || currentAdx is not decimal strength || strength <= prior
			|| CurrentTime.TimeOfDay < SessionStart || CurrentTime.TimeOfDay >= SessionEnd)
			return;
		var isHighCross = close <= _previousDayHigh && candle.ClosePrice > _previousDayHigh;
		var isLowCross = close <= _previousDayLow && candle.ClosePrice > _previousDayLow;
		if (!isHighCross && !isLowCross)
			return;
		_entryDate = CurrentTime.Date;
		Submit(Sides.Buy, Volume, "Previous-day ADX breakout");
	}
}