GitHub で見る

Previous Day High Low Long

ロング専用です。UTCセッション内で終値が前UTC日の観測highまたはlowを上抜け、標準ADXが上昇したとき入ります。実際の約定からの利益目標、観測価格トレーリング、セッション終了で決済します。

水準とエントリー

確定足のみ、既定は5分足。UTCの足開始日でhigh/lowを集計し、次の日付開始時に前日の観測最高high・最低lowを固定します。前日の最初の観測足は午前0時開始で、日付が連続している必要があります。最初の部分日や欠落日に水準を仮定しません。現在日の足は前日水準を拡張しません。

highクロスはpreviousClose <= previousDayHigh && close > previousDayHigh。lowも前日lowを上方向に横断します。標準AdxLength=11のADXが形成済みで、前の形成済み値より厳密に大きいことが必要です。追加のADXしきい値やDI方向フィルターはありません。

ポジションなしからVolumeの成行買いで、注文UTC時刻が[SessionStart,SessionEnd)内、既定09:30–16:00です。毎確定足で終値・日・ADXの履歴を更新し、拒否したクロスを後に持ち越しません。決済後の新しいクロスで再エントリーでき、日次回数制限や6時間待機はありません。ショートを開きません。

保護と終了

MaxProfit=150、MaxStopLoss=15は絶対価格距離で、百分率や銘柄の価格刻みではありません。実際の買い約定でentryPriceとhighWaterを開始し、追加の部分買いは出来高加重平均価格を更新します。減少時は基準を保持し、全決済でリセットします。

正の執行可能bidまたは確定closeでhighWaterを以前と現在価格の最大値に更新します。価格≤highWater-MaxStopLossまたは≥entryPrice+MaxProfitなら実際のロング全量を成行売りします。優先順位はセッション終了、トレーリング、利益目標です。足のhighを架空の執行可能気配に使いません。

1分の市場時刻タイマー、bid、確定足がSessionEnd以降または次のUTC日付に保有を閉じます。時刻は同じUTC日内で順序を持ち、00:00–24:00は翌午前0時終了です。未完了注文は重複を防ぎ、日・ADX履歴は更新を続けます。決済した足コールバックは再エントリーせず戻ります。

ローカル監視で、取引所ネイティブ逆指値は送信しません。足の間の気配による保護にはLevel1 bidが必要で、なければcloseを検査します。約定価格は発動しきい値と異なる場合があります。

パラメーター 既定値
MaxProfit / MaxStopLoss 150 / 15、正の絶対価格距離
AdxLength 11、正
SessionStart / SessionEnd 09:30 / 16:00 UTC、開始を含み終了を含まない
CandleType 5分

C#/Pythonは既存BTC/TON履歴から日を集計し、Wilder ATR/DI/DX/ADXを独立計算します。各実際のエントリー・決済、前日両水準、ADX拒否、開始時の部分履歴、実約定、気配ストップ・目標、強制セッション終了を検証します。収益性は主張しません。

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