Session bars

Implemented: --session and config session select eth, rth, or session for 1d/1w time bars on the plain run path. The last value is an adapter alias for the symbol’s declared trading day; returned bars name the actual era. This is separate from intraday session-hours/session-days filtering, which is not implemented: the config keys are accepted with a warning and ignored.

Custom session windows

A session can also name an exchange-local window, custom:HHMM-HHMM (GLE-397). For example, --session custom:0930-1100 asks for one bar per trade date, built from the trades between 09:30 and 11:00 exchange time. A close earlier than the open wraps past midnight, so custom:1700-0400 is an overnight window. A custom window serves 1d series only. An adapter that serves any such window lists the entry custom in its SupportedSessions. data.ParseCustomSession parses a name, and data.SessionServed is the session gate that the data SDK and the engine both apply. A malformed name is refused before any process starts: one that is not HHMM-HHMM (hours 00 to 23, minutes 00 to 59), one of zero width, and a multi-window name joined with +. A well-formed name is refused by the session gate when the adapter does not list custom. The bundled adapters serve no custom window. marketfeed does (GLE-402) for dated contracts and other session-bearing series: it derives each window bar at read time from its 1s era, keyed by the symbol’s own trade dates. It refuses a window that cannot fit inside a trade date, a bare futures root, a :cont composite, 1w, a path and an unbounded request. Each bar’s BarSession.SessionKind is the window name, or rth/eth when the window is exactly the registry’s RTH or ETH string, because then the stored era is served.

Example. make run-customsession prints a week of the March 2026 E-mini contract’s settlement-anchored day session, 08:30 to 15:15 Chicago time, which rth (08:30 to 16:00) deliberately is not. CUSTOMSESSION=rth prints the regular session for comparison. The opens agree, but the custom bars stop at the 15:15 settlement window, and their closes and volumes differ:

$ make run-customsession
[printbars] [bar 1] fut:XCME:ES:H26 2026-02-09T15:15:01 CST (-06:00) O=6935.5 H=7000.5 L=6924.25 C=6986.25 V=911105
[printbars] [bar 2] fut:XCME:ES:H26 2026-02-10T15:15:01 CST (-06:00) O=6989.75 H=7006.5 L=6957 C=6966.5 V=968019
[printbars] [bar 3] fut:XCME:ES:H26 2026-02-11T15:15:01 CST (-06:00) O=7003.75 H=7011.5 L=6931 C=6963.25 V=1.246295e+06
...
  bars:       5 main after replicate (+ 0 warmup), 5 loop, 0 orders

$ make run-customsession CUSTOMSESSION=rth
[printbars] [bar 1] fut:XCME:ES:H26 2026-02-09T15:59:59 CST (-06:00) O=6935.5 H=7000.5 L=6924.25 C=6984 V=919856
[printbars] [bar 2] fut:XCME:ES:H26 2026-02-10T16:00:00 CST (-06:00) O=6989.75 H=7006.5 L=6957 C=6970.5 V=979014
[printbars] [bar 3] fut:XCME:ES:H26 2026-02-11T16:00:00 CST (-06:00) O=7003.75 H=7011.5 L=6931 C=6963 V=1.257575e+06
...

Example: a custom window as a reference series. strategies/demo/go/sessionref (make run-sessionref; GLE-407) trades the same contract’s hourly bars and declares the settlement-anchored day session as a daily reference series, ref, so each hourly decision reads the latest completed session’s close:

func (s *SessionRef) DataRequirements() []algolang.DataRequirement {
    return []algolang.DataRequirement{{
        SeriesID: "ref", Symbol: s.symbol, Lookback: 2,
        Schema: algolang.DataSchemaSpec{Kind: algolang.Bars, BarKind: algolang.BarsTime,
            Interval: "1d", Session: s.Window}, // Window defaults to custom:0830-1515
    }}
}

Flat, it buys one contract when an hourly close is more than Band (0.2%) above the reference; long, it sells when a close falls back below it. The reference series is pre-rolled, so the first hourly bar already has one. A reference bar completes when its window ends, at 15:15 Chicago (21:15:01 UTC), or at the 12:00 early close on a holiday such as Presidents’ Day, when it closes with the 18:00 UTC hourly bar and, as a co-closing secondary series, reaches the strategy first:

$ make run-sessionref
[sessionref] INFO: sessionref: 2026-02-02T15:00:00Z close=6974.75 ref=6962.75 ref_ok=true pos=0 target=0
[sessionref] INFO: sessionref: 2026-02-02T16:00:00Z close=6995.25 ref=6962.75 ref_ok=true pos=0 target=1
...
[sessionref] INFO: sessionref: 2026-02-02T21:00:00Z close=7003.00 ref=6962.75 ref_ok=true pos=1 target=1
[sessionref] INFO: sessionref: 2026-02-02T22:00:00Z close=7003.25 ref=7004.25 ref_ok=true pos=1 target=0
...
[sessionref] INFO: sessionref: 2026-02-16T17:00:00Z close=6852.50 ref=6853.00 ref_ok=true pos=0 target=0
[sessionref] INFO: sessionref: 2026-02-16T18:00:00Z close=6854.25 ref=6854.25 ref_ok=true pos=0 target=0
...
  instance:   1 (fut:XCME:ES:H26@1h + fut:XCME:ES:H26@1d-custom:0830-1515), 38 orders
  bars:       461 main after replicate (+ 4 warmup), 461 loop, 38 orders
              38 fills, commission 0.00, realized PnL -8262.50

The first hourly bar reads Friday 30 January’s session close, 6962.75, from the pre-roll. The 22:00 bar on 2 February reads that day’s session, which closed at 21:15:01 UTC, and exits. On Presidents’ Day the 18:00 hourly bar reads the session that closed with it. The example shows the mechanism, not an edge: over February it loses.

An end-to-end test (engine/gle407_sessionref_archive_test.go) runs the example over the archive and checks every decision’s reference against the custom-window bars the adapter serves, the reference on the first bar, and a fill for every change of target. A custom window declared to an adapter that does not list custom is refused before the first bar (engine/gle407_e2e_test.go).

GetDataRequest.session travels through DataClient.FetchBars, adapter capability checks, and cache identity. SessionProvider.SupportedSessions advertises supported containers. Automatic negotiation demotes DBN; forced DBN refuses sessions because its OHLCV record cannot preserve trade dates and honest session boundaries. Protobuf and CSV carry session metadata to the strategy. Plain strategy CSV appends the session token as field ten, retaining default series-id and late fields when necessary.

Each BarSession carries trade_date, actual session_kind, optional settle gated by has_settle, early_close, and close_is_settlement. Zero settlement is a price, not missing data. Epoch bars carry no session metadata. Weekly trade dates identify the week’s Monday.

Request membership remains start <= ts_close < end. Session timestamps are the first and last constituent instants, not nominal exchange hours. Use trade_date as the date identity; a daily session is not constrained to 24 hours. An ETH date can legitimately have no RTH bar when the exchange closed before regular hours opened. Do not invent or forward-fill an RTH bar in that case.

Daily-session warmup widens the calendar span to account for non-trading days, then keeps the newest requested number of bars. Weekly warmup does not get the daily 7/5 widening. Warmup remains best effort and reports a shortfall when the source lacks sufficient bars.

On the continuous path (GLE-324, D5 = E): a continuous run at 1d or 1w (a bare root or a :cont composite) reads the feed’s session composite, fut:<VENUE>:<ROOT>:cont:adj=<method> under GetDataRequest.session: one trade-date bar per session under the series’ session, eth when it names none, with the BarSession block delivered to the strategy as a dated contract’s session run delivers it. Under GLE-274’s schedule a roll cut is a trade-date boundary, so no session bar straddles a cut. --continuous-clock epoch (config continuous-clock) keeps UTC epoch buckets at every interval and takes no session; a session together with the epoch clock is refused before any spawn, as is an intraday grid on the continuous path. The adapter must advertise the trading-day session the run reads (marketfeed advertises eth, rth and session); one that does not is refused after the roll-schedule check, and when the series named no session the refusal names --continuous-clock epoch as the way out. This is the schedule-backed composite product D3 named as the reopening vehicle (feed-owned proposal), not a session thread through the bare-root multiplex, which ER3 (GLE-325) deleted.

Current restrictions: replication with a session is refused. A trading-day session refuses the intra-bar path (include-path or path-interval: “a trading-day session cannot carry the intra-bar path”); intraday grids accept it.

Open contract: a contract with some derived trade years and some refused years needs an explicit partial-coverage policy before continuous-session consumption. The historical alternatives were fail the request, serve the available years with logging, or serve with a typed boundary/degradation signal. None is silently selected by this documentation change. The September archive review also flagged crypto session-calendar corrections; archive measurements in the planned-work chapter are dated evidence, not a claim that the external archive is currently repaired.

Implementation anchors: engine/session_request_test.go, engine/warmup_path_test.go, sdk/data/session_test.go, sdk/strategy/session_bar_test.go, and the CSV session codec tests.