Previous Day High Low Long
只做多:在UTC入场时段,close上穿前一UTC日的观测high或low,且原生ADX上升。通过实际成交价利润目标、观测价格跟踪止损或时段结束退出。
水平与入场
只用已完成蜡烛,默认五分钟。按UTC开盘日期汇总high/low,在下个日期开始时固定前日观测最高high和最低low。前日首根观测蜡烛必须从午夜开始且日期连续;初始部分日期或缺失日期不提供虚构水平。当前日蜡烛不扩大前日水平。
high信号:previousClose <= previousDayHigh && close > previousDayHigh;low同样向上穿越前日low。原生AdxLength=11的ADX必须形成且严格大于前一形成值,无额外ADX阈值或DI方向过滤。
从空仓发Volume市价买单,提交UTC时刻在[SessionStart,SessionEnd)内,默认09:30–16:00。每根完成蜡烛更新close、日和ADX,被拒绝穿越不延期。平仓后的新穿越可再入场,无日次入场限制或六小时冷却,不开空。
保护与结束
MaxProfit=150、MaxStopLoss=15是绝对价格距离,不是百分比或价格步长。实际买入成交初始化entryPrice和highWater;追加部分买入更新成交量加权entryPrice,减仓保留基础,全平重置。
每个正的可执行bid或已完成close将highWater更新为历史值与当前价的最大值。价格≤highWater-MaxStopLoss或≥entryPrice+MaxProfit时市价卖出实际多头全量。优先级是时段结束、跟踪止损、利润目标,不将蜡烛high当作可执行报价。
每分钟市场时间定时器、bid和完成蜡烛在/超过SessionEnd或下个UTC日期关闭持仓。时段需在同一UTC日按顺序设置;00:00–24:00在下个午夜结束。未完成订单阻止重复,日/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");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Array, ArgumentException, Decimal, TimeSpan
from StockSharp.Messages import CandleStates, DataType, ITickTradeMessage, Level1Fields, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Indicators import AverageDirectionalIndex, IIndicator
from StockSharp.Algo.Strategies import Strategy
class previous_day_high_low_long_strategy(Strategy):
def __init__(self):
super(previous_day_high_low_long_strategy, self).__init__()
self._max_profit = self.Param("MaxProfit", Decimal(150)).SetGreaterThanZero() \
.SetDisplay("Maximum Profit", "Target distance above actual entry price, in absolute price units", "Protection")
self._max_stop_loss = self.Param("MaxStopLoss", Decimal(15)).SetGreaterThanZero() \
.SetDisplay("Maximum Stop Loss", "Trailing distance below highest observed price, in absolute price units", "Protection")
self._adx_length = self.Param("AdxLength", 11).SetGreaterThanZero() \
.SetDisplay("ADX Length", "Native directional-strength period; current ADX must rise", "Indicators")
self._session_start = self.Param("SessionStart", TimeSpan.FromMinutes(570)) \
.SetDisplay("Session Start", "Inclusive UTC entry time", "Session")
self._session_end = self.Param("SessionEnd", TimeSpan.FromHours(16)) \
.SetDisplay("Session End", "Exclusive UTC entry time and mandatory close", "Session")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Finished candles supplying UTC day levels and ADX", "General")
tick_type = clr.GetClrType(ITickTradeMessage)
self._tick_price = tick_type.GetProperty("Price")
self._tick_volume = tick_type.GetProperty("Volume")
self._reset_state()
self.OrderRegistering += self._observe_order
self.OwnTradeReceived += self._observe_fill
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def _reset_state(self):
self._day = self._previous_close = self._previous_adx = self._entry_date = None
self._day_high = self._day_low = self._previous_day_high = self._previous_day_low = Decimal.Zero
self._is_day_start_observed = self._is_previous_day_available = False
self._filled_position = self._entry_price = self._extreme = Decimal.Zero
self._pending_order = None
def OnReseted(self):
super(previous_day_high_low_long_strategy, self).OnReseted()
self._reset_state()
def _observe_order(self, order):
self._pending_order = order
def OnStarted2(self, time):
super(previous_day_high_low_long_strategy, self).OnStarted2(time)
if self._max_profit.Value <= 0 or self._max_stop_loss.Value <= 0 or self._adx_length.Value <= 0 \
or self._session_start.Value < TimeSpan.Zero or self._session_start.Value >= self._session_end.Value \
or self._session_end.Value > TimeSpan.FromDays(1):
raise ArgumentException("Risk distances and ADX period must be positive; session times must be ordered within a UTC day.")
self._reset_state()
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = Level1Fields.BestBidPrice
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
self.StartTimer(TimeSpan.FromMinutes(1), self._try_session_exit)
adx = AverageDirectionalIndex()
adx.Length = self._adx_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(Array[IIndicator]([adx]), self.OnProcess, True).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _is_pending(self):
order = self._pending_order
return order is not None and order.State != OrderStates.Done and order.State != OrderStates.Failed
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
order.Comment = comment
self.RegisterOrder(order)
def _try_session_exit(self):
date = self._entry_date
is_over = date is not None and (self.CurrentTime.Date > date or self.CurrentTime.TimeOfDay >= self._session_end.Value)
if not is_over or self.Position <= 0 or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return False
self._submit(Sides.Sell, self.Position, "Previous-day session close")
return True
def _observe_fill(self, subscription, trade):
previous = self._filled_position
price = self._tick_price.GetValue(trade.Trade)
volume = self._tick_volume.GetValue(trade.Trade)
self._filled_position += volume if trade.Order.Side == Sides.Buy else -volume
if trade.Order.Side == Sides.Buy and self._filled_position > 0:
self._entry_price = (self._entry_price * previous + price * volume) / self._filled_position
self._extreme = price if previous == 0 else max(self._extreme, price)
if self._filled_position == 0:
self._entry_price = self._extreme = Decimal.Zero
def _observe_price(self, price):
if self._try_session_exit() or self.Position <= 0 or self._entry_price <= 0 \
or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return
self._extreme = max(self._extreme, price)
comment = "Previous-day trailing stop" if price <= self._extreme - self._max_stop_loss.Value \
else "Previous-day profit target" if price >= self._entry_price + self._max_profit.Value else None
if comment is not None:
self._submit(Sides.Sell, self.Position, comment)
def _observe_quote(self, quote):
if quote.Changes.ContainsKey(Level1Fields.BestBidPrice) and quote.Changes[Level1Fields.BestBidPrice] > 0:
self._observe_price(quote.Changes[Level1Fields.BestBidPrice])
def _observe_day(self, candle):
date = candle.OpenTime.Date
if self._day != date:
self._is_previous_day_available = self._is_day_start_observed and self._day == date.AddDays(-1)
self._previous_day_high = self._day_high
self._previous_day_low = self._day_low
self._day = date
self._day_high = candle.HighPrice
self._day_low = candle.LowPrice
self._is_day_start_observed = candle.OpenTime.TimeOfDay == TimeSpan.Zero
else:
self._day_high = max(self._day_high, candle.HighPrice)
self._day_low = min(self._day_low, candle.LowPrice)
def OnProcess(self, candle, values):
if candle.State != CandleStates.Finished:
return
previous_close = self._previous_close
self._previous_close = candle.ClosePrice
self._observe_day(candle)
previous_adx = self._previous_adx
current_adx = values[0].MovingAverage if values[0].IsFormed else None
if current_adx is not None:
self._previous_adx = current_adx
if self.Position > 0:
self._observe_price(candle.ClosePrice)
return
if self.Position != 0 or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading() \
or not self._is_previous_day_available or previous_close is None or previous_adx is None \
or current_adx is None or current_adx <= previous_adx \
or self.CurrentTime.TimeOfDay < self._session_start.Value or self.CurrentTime.TimeOfDay >= self._session_end.Value:
return
high_cross = previous_close <= self._previous_day_high and candle.ClosePrice > self._previous_day_high
low_cross = previous_close <= self._previous_day_low and candle.ClosePrice > self._previous_day_low
if not high_cross and not low_cross:
return
self._entry_date = self.CurrentTime.Date
self._submit(Sides.Buy, self.Volume, "Previous-day ADX breakout")
def CreateClone(self):
return previous_day_high_low_long_strategy()