#region Using declarations using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Windows.Media; using NinjaTrader.Data; using NinjaTrader.Gui.Tools; using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript.DrawingTools; #endregion // Jason Opening Range 是只显示市场结构的图表指标。 // 它不生成交易信号,也不执行任何交易操作。 namespace NinjaTrader.NinjaScript.Indicators { public class JasonOpeningRange : Indicator { private SessionIterator sessionIterator; private DateTime sessionBegin; private DateTime sessionEnd; private DateTime rangeBegin; private DateTime rangeEnd; private string sessionTag; private int rangeStartBar; private double rangeHigh; private double rangeLow; private bool hasRangeData; private bool sessionInitialized; private bool rangeWindowAvailable; private bool hasReportedSessionError; protected override void OnStateChange() { if (State == State.SetDefaults) { Description = "显示每个 Trading Hours Session 的 Opening Range High、Low 和 Mid。"; Name = "JasonOpeningRange"; Calculate = Calculate.OnBarClose; IsOverlay = true; DisplayInDataBox = false; PaintPriceMarkers = false; StartTime = 93000; EndTime = 100000; ShowHigh = true; ShowLow = true; ShowMid = true; ShowLabels = true; LineWidth = 2; LineStyle = DashStyleHelper.Solid; } else if (State == State.DataLoaded) { // SessionIterator 只在 Bars 数据可用后创建。 try { sessionIterator = new SessionIterator(Bars); } catch (Exception exception) { sessionIterator = null; Print(Name + ": 无法初始化 SessionIterator: " + exception.Message); } } } protected override void OnBarUpdate() { try { DateTime barTime = Time[0]; // NinjaTrader 会在每个 Trading Hours Session 的首根 Bar 标记此属性。 // 对首次加载的历史 Bar 也会重置,因此历史图表可从数据序列起点计算。 if (Bars.IsFirstBarOfSession || !sessionInitialized) ResetSession(barTime); if (!rangeWindowAvailable) return; // 时间范围使用图表 Bar 的时间戳,左闭右开。 // 例如 09:30–10:00 会纳入时间戳从 09:30 到 09:59:59 的 Bar。 if (barTime >= rangeBegin && barTime < rangeEnd) { if (!hasRangeData) { rangeHigh = High[0]; rangeLow = Low[0]; rangeStartBar = CurrentBar; hasRangeData = true; } else { rangeHigh = Math.Max(rangeHigh, High[0]); rangeLow = Math.Min(rangeLow, Low[0]); } } // 等到窗口结束才绘制完整区间;同一 Session 复用相同 Tag, // 每根新 Bar 只更新已有对象,不创建重复绘图对象。 if (!hasRangeData || barTime < rangeEnd) return; double rangeMid = (rangeHigh + rangeLow) / 2.0; DrawLevel("High", "ORH", rangeHigh, Brushes.SteelBlue, ShowHigh); DrawLevel("Low", "ORL", rangeLow, Brushes.IndianRed, ShowLow); DrawLevel("Mid", "ORM", rangeMid, Brushes.DarkGray, ShowMid); } catch (Exception exception) { ReportSessionError("Opening Range 计算或绘图失败", exception); } } /// /// 清空上一 Session 的状态,并从 Trading Hours 模板解析本 Session 的 /// 实际起止时间以及其中第一个完整的每日 Opening Range 时间窗口。 /// private void ResetSession(DateTime barTime) { sessionInitialized = true; rangeWindowAvailable = false; hasRangeData = false; rangeStartBar = -1; rangeHigh = double.MinValue; rangeLow = double.MaxValue; hasReportedSessionError = false; sessionBegin = DateTime.MinValue; sessionEnd = DateTime.MinValue; rangeBegin = DateTime.MinValue; rangeEnd = DateTime.MinValue; sessionTag = barTime.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture); try { if (sessionIterator == null) { ReportSessionError("SessionIterator 不可用,当前 Session 已跳过", null); return; } if (!TryConvertTime(StartTime, out TimeSpan startOffset) || !TryConvertTime(EndTime, out TimeSpan endOffset) || startOffset == endOffset) { ReportSessionError("开始和结束时间必须是不同的有效 HHmmss 时间", null); return; } if (!sessionIterator.GetNextSession(barTime, true)) { ReportSessionError("无法读取当前 Trading Hours Session", null); return; } sessionBegin = sessionIterator.ActualSessionBegin; sessionEnd = sessionIterator.ActualSessionEnd; if (sessionEnd <= sessionBegin) { ReportSessionError("Trading Hours Session 的起止时间无效", null); return; } // Trading Hours 可以跨越午夜。逐日查找本 Session 内完整的时间窗口, // 因此晚间开始的 Session 也可以在次日早晨形成 Opening Range。 for (DateTime candidateDate = sessionBegin.Date.AddDays(-1); candidateDate <= sessionEnd.Date; candidateDate = candidateDate.AddDays(1)) { DateTime candidateBegin = candidateDate.Date + startOffset; DateTime candidateEnd = candidateDate.Date + endOffset; // 结束时间小于开始时间时,窗口跨午夜;相等输入已在上方拒绝。 if (endOffset < startOffset) candidateEnd = candidateEnd.AddDays(1); bool isInsideSession = candidateBegin >= sessionBegin && candidateBegin < sessionEnd && candidateEnd <= sessionEnd; if (!isInsideSession) continue; rangeBegin = candidateBegin; rangeEnd = candidateEnd; sessionTag = sessionBegin.ToString( "yyyyMMdd_HHmmss", CultureInfo.InvariantCulture); rangeWindowAvailable = true; return; } // 若 Trading Hours 模板不覆盖所设窗口,保持安静且不绘图。 rangeWindowAvailable = false; } catch (Exception exception) { ReportSessionError("读取 Session 时间失败,当前 Session 已跳过", exception); } } /// /// 将 NinjaTrader 常用 HHmmss 整数解析为一天内的时间。 /// private bool TryConvertTime(int timeValue, out TimeSpan timeOffset) { timeOffset = TimeSpan.Zero; if (timeValue < 0 || timeValue > 235959) return false; int hours = timeValue / 10000; int minutes = (timeValue / 100) % 100; int seconds = timeValue % 100; if (hours > 23 || minutes > 59 || seconds > 59) return false; timeOffset = new TimeSpan(hours, minutes, seconds); return true; } /// /// 绘制或更新一条价格线及可选标签。Tag 按 Session 和水平名称固定。 /// private void DrawLevel( string levelName, string label, double price, Brush brush, bool isVisible) { string lineTag = "JasonOpeningRange." + sessionTag + "." + levelName; string labelTag = lineTag + ".Label"; if (!isVisible) { RemoveDrawObject(lineTag); RemoveDrawObject(labelTag); return; } int startBarsAgo = CurrentBar - rangeStartBar; Draw.Line( this, lineTag, false, startBarsAgo, price, 0, price, brush, LineStyle, LineWidth); if (ShowLabels) Draw.Text(this, labelTag, label, 0, price, brush); else RemoveDrawObject(labelTag); } /// /// 限制每个 Session 的错误输出次数,避免异常情况下重复刷屏。 /// private void ReportSessionError(string message, Exception exception) { if (hasReportedSessionError) return; hasReportedSessionError = true; string detail = exception == null ? string.Empty : ": " + exception.Message; Print(Name + ": " + message + detail); } #region User parameters [NinjaScriptProperty] [Range(0, 235959)] [Display(Name = "Start Time (HHmmss)", Description = "图表时间戳中的 Opening Range 开始时间。", Order = 1, GroupName = "Opening Range")] public int StartTime { get; set; } [NinjaScriptProperty] [Range(0, 235959)] [Display(Name = "End Time (HHmmss)", Description = "图表时间戳中的 Opening Range 结束时间,不包含结束时间戳。", Order = 2, GroupName = "Opening Range")] public int EndTime { get; set; } [NinjaScriptProperty] [Display(Name = "Show High", Order = 1, GroupName = "Display")] public bool ShowHigh { get; set; } [NinjaScriptProperty] [Display(Name = "Show Low", Order = 2, GroupName = "Display")] public bool ShowLow { get; set; } [NinjaScriptProperty] [Display(Name = "Show Mid", Order = 3, GroupName = "Display")] public bool ShowMid { get; set; } [NinjaScriptProperty] [Display(Name = "Show Labels", Order = 4, GroupName = "Display")] public bool ShowLabels { get; set; } [NinjaScriptProperty] [Range(1, 5)] [Display(Name = "Line Width", Order = 5, GroupName = "Display")] public int LineWidth { get; set; } [NinjaScriptProperty] [Display(Name = "Line Style", Order = 6, GroupName = "Display")] public DashStyleHelper LineStyle { get; set; } #endregion } }