Открыть на GitHub

Previous Day High Low Long

Только long по пересечению вверх наблюдаемых high или low предыдущего UTC-дня, с растущим штатным ADX внутри UTC-сессии. Выход по цели от фактического исполнения, трейлингу наблюдаемой цены или окончанию сессии.

Уровни и вход

Только завершённые свечи, по умолчанию пятиминутные. High/low группируются по UTC-дате открытия свечи. При начале следующей даты фиксируются наблюдаемые максимальный high и минимальный low прошлого дня. Он доступен, только если его первая наблюдаемая свеча открылась в полночь и даты соседние. Для неполного начального дня или пропущенной даты уровни не выдумываются.

Пробой high: previousClose <= previousDayHigh && close > previousDayHigh; для low — такое же пересечение вверх его уровня. Свечи текущего дня не изменяют эти фиксированные уровни. Штатный ADX с AdxLength=11 должен сформироваться и строго превысить своё предыдущее сформированное значение. Дополнительного порога ADX и проверки направления DI нет.

Вход только из нулевой позиции рыночной покупкой Volume, если UTC-время отправки входит в [SessionStart, SessionEnd), по умолчанию 09:30–16:00. Отклонённый пробой не откладывается: история close, дня и ADX обновляется на каждой завершённой свече. После закрытия возможен вход по новому пересечению; суточного лимита и шестичасового cooldown нет. Short не открывается.

Защита и завершение сессии

Расстояния заданы в абсолютных единицах цены: MaxProfit=150, MaxStopLoss=15, а не процентах или шагах цены инструмента. Начальная цена и максимум берутся из фактического исполнения покупки; дополнительные частичные покупки обновляют средневзвешенную цену входа. Уменьшение позиции сохраняет её базу, нулевая позиция сбрасывает её.

Каждый положительный исполнимый bid или завершённый close обновляет наблюдаемый максимум до большего из прошлого значения и текущей цены. Полный фактический long закрывается рыночной продажей при цене ≤ highWater - MaxStopLoss либо ≥ entryPrice + MaxProfit. Приоритет: конец сессии, трейлинг, цель прибыли. High свечи не подставляется вместо наблюдаемой исполнимой цены.

Минутный таймер рыночного времени, bid и завершённые свечи закрывают long при/после SessionEnd либо на следующей UTC-дате. Времена упорядочены внутри одного дня; 00:00–24:00 закрывается в следующую полночь. Незавершённый ордер блокирует дубли до терминального состояния; история дней/ADX продолжает обновляться. Свечной callback после закрытия возвращается без повторного входа.

Это локальный мониторинг, не биржевой стоп-ордер. Для intrabar-защиты по котировкам нужен Level1 bid; без него доступны закрытия свечей. Цена исполнения может отличаться от порога.

Настройки и проверки

Параметр Значение по умолчанию
MaxProfit / MaxStopLoss 150 / 15, положительные абсолютные расстояния цены
AdxLength 11, положительный
SessionStart / SessionEnd 09:30 / 16:00 UTC, начало включено, конец исключён
CandleType 5 минут

Тесты C#/Python независимо группируют наблюдаемые дни и рассчитывают Wilder ATR/DI/DX/ADX. Проверяются каждый фактический вход/выход, оба уровня прошлого дня, отказы растущего ADX, неполное начало истории, фактические исполнения, трейлинг/цель по котировкам и обязательное закрытие сессии на имеющемся архиве BTC/TON. Доходность не заявляется.

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");
	}
}