#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
}
}