Trading hours & sessions

Trading hours & sessions

Trading hours & sessions

Ask "when does this bar's session begin and end?", "is this a new trading day?", and "which trading day does this bar belong to?" — holiday- and overnight-correct, without hand-rolling time-of-day arithmetic. This is the TradeStrike equivalent of NinjaTrader's SessionIterator.

What it is & why it exists

Session-anchored indicators — daily OHL, prior-day OHLC, initial balance, pivots, VWAP, net change — all need the same thing: a reliable notion of "trading day" and "session open" that survives overnight sessions, week wraps, DST and holidays. A naive time-of-day window gets all of those wrong. The trading-hours model centralizes the rule so every consumer (charts, Market Analyzer, SuperDOM, strategies) agrees on the same session boundaries for the same instrument.

As a plugin author you reach for it through your IIndicatorContext: read the bound TradingHours template, ask IsFirstBarOfSession, or create an ISessionIterator for richer queries. Everything is dependency-free — when no calendar is configured the platform falls back to a 24/7 template, so your code runs with zero host wiring.

The contracts live in TradeStrike.Pipeline.Indicators.TradingHours (plus the lightweight time-of-day helper SessionWindow in TradeStrike.Pipeline.Indicators.Sessions).

The session model

A TradingTime is a minute-of-day stored NinjaTrader-style as an HHMM integer (1700 = 17:00). Its legal range is 0..2400, where 2400 is the end-of-day sentinel meaning "24:00 / the next day's 00:00" — so a session can say "ends at midnight of the following day" without juggling a rollover in the value.

TradingTime.csusing System;
using TradeStrike.Pipeline.Indicators.TradingHours;

public readonly record struct TradingTime
{
    public TradingTime(int hhmm);                 // 0..2400; throws for an invalid hour/minute

    public int Hhmm { get; }                       // raw HHMM value
    public int Hour { get; }                       // 0..24 (24 only for the sentinel)
    public int Minute { get; }                     // 0..59
    public int MinutesFromMidnight { get; }        // 0..1440 (the 2400 sentinel maps to 1440)
    public bool IsEndOfDaySentinel { get; }        // true for 2400

    public static TradingTime FromMinutes(int minutesFromMidnight);   // inverse of MinutesFromMidnight
    public static TradingTime FromTimeOnly(TimeOnly time);
    public TimeOnly ToTimeOnly();
    public override string ToString();              // "HH:mm" (the sentinel renders as "24:00")
}

A TradingSession is one weekly-recurring segment. It opens at BeginDay + BeginTime and closes at EndDay + EndTime, both in the owning template's exchange time zone. A session may cross midnight and/or wrap the week. TradingDay is the day-of-week the segment's volume / OHLC is attributed to — for CME's Sunday-evening ETH segment that is Monday.

TradingSession.csusing System;
using TradeStrike.Pipeline.Indicators.TradingHours;

public readonly record struct TradingSession(
    DayOfWeek BeginDay,
    TradingTime BeginTime,
    DayOfWeek EndDay,
    TradingTime EndTime,
    DayOfWeek TradingDay)
{
    public int BeginOffsetMinutes { get; }   // minutes from Sunday 00:00 to the open
    public int EndOffsetMinutes { get; }      // close, with the week wrap folded in (always > begin)
    public int DurationMinutes { get; }       // strictly positive, at most a full week (10080)
}
One code path for overnight and intraday. All boundary maths is done in a single "minutes from the start of the week" space with the week wrap folded in once. There is deliberately no start<end vs start>end branching, so overnight CME ETH and a daytime RTH session resolve through the same logic.

Templates

A TradingHoursTemplate is a named, immutable, reusable definition — the TradeStrike equivalent of a NinjaTrader "Trading Hours" template. It is the single source of truth for an exchange's weekly schedule, its holiday calendar and the exchange time zone the session times are expressed in. It lives in the contracts assembly so you can read it via IIndicatorContext.TradingHours without referencing the host engine.

TradingHoursTemplate.csusing System;
using System.Collections.Generic;
using TradeStrike.Pipeline.Indicators.TradingHours;

public sealed class TradingHoursTemplate
{
    public string Name { get; }                                 // also its lookup key in the store
    public IReadOnlyList<TradingSession> Sessions { get; }       // weekly-recurring sessions (may be empty)
    public IReadOnlyList<HolidayRule> Holidays { get; }          // holiday / partial-holiday calendar
    public TimeZoneInfo ExchangeTimeZone { get; }               // zone the TradingTimes are expressed in

    public TradingHoursTemplate(
        string name,
        IReadOnlyList<TradingSession> sessions,
        IReadOnlyList<HolidayRule> holidays,
        TimeZoneInfo exchangeTimeZone);

    // Convenience factory that resolves a Windows time-zone id (e.g. "Central Standard Time").
    public static TradingHoursTemplate Create(
        string name,
        IReadOnlyList<TradingSession> sessions,
        IReadOnlyList<HolidayRule> holidays,
        string windowsTimeZoneId);
}

TradingHoursTemplates ships canonical, code-defined fixtures — the always-available baselines that work with zero host wiring (the full per-exchange catalogue ships via the seeded store).

TradingHoursTemplates.csusing TradeStrike.Pipeline.Indicators.TradingHours;

// The store / resolver fallback name — agreed in one place across engine, store and resolver.
string fallback = TradingHoursTemplates.TwentyFourSevenName;       // "24 / 7"

TradingHoursTemplate t1 = TradingHoursTemplates.TwentyFourSeven(); // 7 daily 00:00→00:00 sessions, UTC
TradingHoursTemplate t2 = TradingHoursTemplates.TwentyFourFive();  // Mon–Fri full days, UTC
TradingHoursTemplate t3 = TradingHoursTemplates.CmeEth();          // Sun 17:00 → Mon 16:00 …, Central time
TradingHoursTemplate t4 = TradingHoursTemplates.CmeRth();          // Mon–Fri 08:30 → 15:00, Central time

Holidays

The five NinjaTrader holiday types are modelled by HolidayKind, each keyed to one exchange trading date by a HolidayRule. A partial holiday carries a Constraint that is itself a full session definition.

HolidayKind.csnamespace TradeStrike.Pipeline.Indicators.TradingHours;

public enum HolidayKind
{
    FullDay,     // exchange fully closed — no session, Constraint is null
    Replace,     // replace all the date's sessions with the rule's single Constraint session
    EarlyClose,  // the closing session ends at the Constraint's EndTime (e.g. 12:15 instead of 16:00)
    LateOpen,    // the opening session begins at the Constraint's BeginTime
    Modify       // both begin and end come from the Constraint (non-standard partials)
}
HolidayRule.csusing System;
using TradeStrike.Pipeline.Indicators.TradingHours;

public readonly record struct HolidayRule(
    DateOnly TradingDate,          // date-only, exchange terms — the date NinjaTrader keys holidays by
    HolidayKind Kind,
    TradingSession? Constraint,    // null for FullDay; the partial/replacement session otherwise
    string? Description);

Resolving a template

ITradingHoursResolver is the single resolution chokepoint that maps an instrument (and an optional explicit template name) to a concrete template, so a given pair always yields the same hours everywhere. It is consumer/venue-agnostic — the only input is a raw instrument id — and never returns null (it falls back to 24/7).

ITradingHoursResolver.csusing System.Collections.Generic;
using TradeStrike.Pipeline.Indicators.TradingHours;

public interface ITradingHoursResolver
{
    // The instrument's default template ("<Use instrument settings>"). Never null.
    TradingHoursTemplate Resolve(string instrumentId);

    // Explicit selection: a known templateName wins; null/blank = the instrument default. Never null.
    TradingHoursTemplate Resolve(string instrumentId, string? templateName);

    // The name of the instrument's default template, for UI display.
    bool TryResolveName(string instrumentId, out string templateName);

    // Every template name the resolver can resolve — for populating a selection UI. Never null.
    IReadOnlyList<string> ListTemplateNames();
}

Catalog-instantiated plugins (indicators, drawing tools) that cannot take a constructor dependency read the process-wide resolver from TradingHoursAmbient — the same ambient-provider pattern used elsewhere in the SDK. The default is the dependency-free NullTradingHoursResolver (24/7 for everything), so tests and any process that never installs a resolver behave deterministically.

TradingHoursAmbient.csusing System;
using TradeStrike.Pipeline.Indicators.TradingHours;

public static class TradingHoursAmbient
{
    public static ITradingHoursResolver Resolver { get; }              // never null; defaults to 24/7
    public static void SetResolver(ITradingHoursResolver? resolver);   // null restores the 24/7 default
    public static IDisposable Use(ITradingHoursResolver resolver);     // scoped install (tests)
}

// Resolve a second instrument's calendar from a multi-symbol indicator:
TradingHoursTemplate esHours = TradingHoursAmbient.Resolver.Resolve("ES 06-26");

The runtime bridge consumes one ITradingHoursProvider per data series to obtain that series' template (the resolver, a per-series override and a fixed test value all implement it). You normally read the resolved template straight off your context rather than implementing this yourself.

ITradingHoursProvider.csusing TradeStrike.Pipeline.Indicators.TradingHours;

public interface ITradingHoursProvider
{
    // The resolved template for the series, or null = "no calendar / 24-7".
    TradingHoursTemplate? GetTemplate();
}

The session iterator

ISessionIterator is a stateful cursor over a schedule — the TradeStrike equivalent of new SessionIterator(Bars). Create one in OnInit / OnDataLoaded from your context and keep it for the series' life. All time arguments and returned Actual* instants are in the chart display timeline ("display-as-Utc-kind"), so they compare directly against bar StartUtc / EndUtc.

ISessionIterator.csusing System;
using TradeStrike.Pipeline.Indicators.TradingHours;

public interface ISessionIterator
{
    DateTime GetTradingDay(DateTime time);                                  // the trading day (date-only, exchange) the instant belongs to
    bool IsNewSession(DateTime time, bool includesEndTimestamp);            // no longer inside the positioned session?
    void GetNextSession(DateTime time, bool includesEndTimestamp);         // advance the cursor + refresh Actual*
    DateTime CalculateTradingDay(DateTime time, bool includesEndTimestamp); // position on the trading day, return ActualTradingDayExchange
    bool IsInSession(DateTime time, bool includesEndTimestamp, bool isIntraday);
    bool IsTradingDayDefined(DateTime date);                                // false on a full-day holiday / non-trading weekday
    DateTime GetTradingDayBeginLocal(DateTime tradingDayExchange);          // display-timeline begin of the whole trading day
    DateTime GetTradingDayEndLocal(DateTime tradingDayExchange);            // display-timeline end (EOD)
    void Reset();                                                            // clear the cursor

    DateTime ActualSessionBegin { get; }        // begin of the positioned session (display timeline)
    DateTime ActualSessionEnd { get; }           // end of the positioned session
    DateTime ActualTradingDayExchange { get; }   // positioned session's trading day (date-only, exchange)
    DateTime ActualTradingDayEndLocal { get; }   // EOD of the positioned trading day (display timeline)
}
The includesEndTimestamp flag. Pass true for time-based series (a bar stamped exactly at the session close still belongs to the closing session) and false for tick / volume / range series. This is the only place the distinction matters.

Your context creates the iterator for you — you do not new it up directly:

IIndicatorContext.cs (trading-hours members)using TradeStrike.Pipeline.Indicators.TradingHours;

// The template bound to this chart's instrument + data series, or null = 24/7.
TradingHoursTemplate? TradingHours { get; }

// A fresh stateful iterator. Pass null for the chart instrument's template, or an
// explicit template for a multi-symbol / cross-instrument indicator.
ISessionIterator? CreateSessionIterator(TradingHoursTemplate? template = null);

// True when the current OnBarUpdate bar is the first bar of a new trading session
// (NinjaTrader's Bars.IsFirstBarOfSession). The host computes it once per dispatched bar.
bool IsFirstBarOfSession { get; }

Per-bar reset helpers

SessionBoundary is the single pure rule for "is this bar the first bar of a session?", shared by the indicator and strategy contexts. It compares the current and previous bar opens over an iterator, using the trading-day key (not a raw time-of-day window) so it is holiday- and overnight-correct.

SessionBoundary.csusing System;
using TradeStrike.Pipeline.Indicators.TradingHours;

public static class SessionBoundary
{
    // In-session AND (first bar OR previous bar was out-of-session OR a different trading day).
    public static bool IsSessionOpen(
        ISessionIterator iterator,
        DateTime currentStartUtc,
        bool hasPreviousBar,
        DateTime previousStartUtc);
}

SessionResetTracker wraps all three reset modes the session-anchored built-ins used to hand-roll (CurrentDayOHL, PriorDayOHLC, InitialBalance, Pivots, NetChange, …) into one stateful, single-threaded helper. Feed it bars in chronological order; OnBar returns true on a new session, and it exposes InSession plus the session's open/close instants. It even folds an exchange settlement window (the CME 16:00–16:05 CT prints) into the just-closed session.

SessionResetTracker.csusing System;
using TradeStrike.Pipeline.Indicators.TradingHours;

public sealed class SessionResetTracker
{
    // instrumentIterator non-null = instrument-trading-hours mode (window args ignored);
    // otherwise sessionAware selects manual-window vs legacy-UTC-date mode.
    public static SessionResetTracker Create(
        ISessionIterator? instrumentIterator, bool sessionAware, TimeOnly start, TimeOnly end);

    public bool InSession { get; }            // did the most recent bar fall inside a session?
    public DateTime SessionBeginUtc { get; }  // open instant of the current session (display-as-Utc)
    public DateTime SessionEndUtc { get; }    // close instant of the current session

    public bool OnBar(DateTime barStartUtc);                    // open-only (tick/live); folds by time window
    public bool OnBar(DateTime barStartUtc, DateTime barEndUtc); // true when this bar opens a new session/day
    public void Reset();
}

For the simplest time-of-day case — a manual session window with no calendar — the pure SessionWindow helper (in TradeStrike.Pipeline.Indicators.Sessions) suffices. It encodes day / overnight / 24-7 by the relationship between start and end.

SessionWindow.csusing System;
using TradeStrike.Pipeline.Indicators.Sessions;

public static class SessionWindow
{
    // Start < End: daytime (Start ≤ t < End). Start > End: overnight (t ≥ Start OR t < End).
    // Start == End: 24/7 (always in-session).
    public static bool IsInSession(DateTime barTime, TimeOnly start, TimeOnly end);

    // The calendar date of the bar's session (rolled back a day for the after-midnight portion of an overnight session).
    public static DateOnly SessionDate(DateTime barTime, TimeOnly start, TimeOnly end);

    // True when barTime is the first bar of a new session.
    public static bool IsSessionOpenBar(DateTime currentTime, DateTime previousTime, TimeOnly start, TimeOnly end);
}

A session-anchored indicator

A complete current-day high / low indicator that resets at each session open. It creates an iterator in OnDataLoaded, uses IsFirstBarOfSession to reset, and falls back gracefully to 24/7 when the host supplies no calendar (CreateSessionIterator always returns a usable iterator from the production context):

CurrentDayRange.csusing System;
using TradeStrike.Pipeline.Indicators;
using TradeStrike.Pipeline.Indicators.TradingHours;

public sealed class CurrentDayRange : IndicatorBase
{
    private ISessionIterator? _sessions;
    private double _high = double.NaN;
    private double _low  = double.NaN;

    protected override void OnDataLoaded(IIndicatorContext ctx)
    {
        // Create once for the chart instrument's template; usable even with no calendar (24/7).
        _sessions = ctx.CreateSessionIterator();
    }

    protected override void OnBarUpdate(IIndicatorContext ctx)
    {
        var bars = ctx.Bars(0);
        var bar = bars[0];

        // Reset the running range at the first bar of a new trading session.
        if (ctx.IsFirstBarOfSession)
        {
            _high = bar.High;
            _low  = bar.Low;
        }
        else
        {
            _high = double.IsNaN(_high) ? bar.High : Math.Max(_high, bar.High);
            _low  = double.IsNaN(_low)  ? bar.Low  : Math.Min(_low,  bar.Low);
        }

        // Which trading day does this bar belong to? (drives a per-day label, accumulator key, …)
        DateTime tradingDay = _sessions?.GetTradingDay(bar.StartUtc).Date ?? bar.StartUtc.Date;

        HighPlot[0] = _high;
        LowPlot[0]  = _low;
    }
}
Multi-symbol indicators. When you compute against a second instrument, resolve its template with TradingHoursAmbient.Resolver.Resolve(symbol) and pass it to ctx.CreateSessionIterator(template) — that instrument's sessions, not the chart's.