Event ordering and calculations

Event ordering and calculations

The engine validates bars and merges series by close timestamp. Bars that close at the same instant follow the co-close order (owner decision GLE-259, built in GLE-262): every regular price series other than the primary goes first, so the primary’s decision at that instant sees a co-closing leg (no one-bar-stale second leg in a spread); then the primary (series 0, the first configured series); then irregular release-stamped series — cot:, macro: and fx:RBA: — which count from the next primary bar, because a release stamped at a bar’s close came after that close’s price. Within a class, series order (the run config, then declared series). Every bar still reaches OnBar (a strategy filters by bar.SeriesID or ctx.IsPrimary). Declared auxiliary series may inform a strategy without joining its tradable universe; out-of-universe orders are denied. Warmup bars populate SDK history without invoking trading decisions. Each series warms up to the depth the SDK buffers for it: its declared DataRequirement.Lookback when a requirement names it, else the global Lookback(); an irregular series counts release rows (widening its window until it holds that many or reaches the archive’s start). An event series (volume, tick, imbalance and the other activity bars; GLE-407) has no fixed span to plan a window from, so its pre-roll widens one: from 30 minutes before the first bar, doubling until it holds the depth, up to a week or the archive’s earliest data. Each pre-roll fetch asks for the series’ own construction, lead-in included, and ends where the series’ first bar opens, so no trade reaches the strategy twice (the corporate-action window widens to cover the week before); the pre-roll’s bars are built over their own window, so their boundaries need not line up with the run’s. A series with no bar inside the run window (a quarterly release in a short run) still warms up, anchored at the run start. A series whose warm-up comes up short is recorded as a warmup_shortfall degradation (stderr summary and degradation.json); the run continues.

Declared trades (GLE-389), statistics and definitions (GLE-406) are not bars and take no place in the co-close order. Before each bar the engine sends one MarketEvents push holding every declared event visible strictly before that bar’s close and not yet sent, in order of visibility instant (ties in declaration order, then series order), after the bar’s account push and before the bar itself. A trade is visible at its ts_recv, a statistic at its publication instant and a definition at its ts_event; an event visible at exactly a bar’s close goes out before the next bar. See Market events and Statistics and definitions as events.

Aligned series (GLE-353) are built in the SDK from this stream and add no events. On each grid bar the SDK appends the view’s sample after the bar joins its own buffer and before OnBar. A co-closing source bar that the co-close order delivers after the grid bar (a primary bar after a declared grid’s, for instance) revises the newest sample within the instant, so samples read on the primary’s bar are final. Before the run, the engine adds one warm-up batch per view, tagged with the view’s id: the source’s history older than its own warm-up, back to the newest bar visible at the grid’s oldest warm-up bar. It goes out immediately before the source’s own warm-up batch, through the same warm-up path, and reaches only the view. A view that the source’s archive cannot fill is recorded as an aligned_shortfall degradation.

For each trading bar, the engine delivers pending rejection cancellations, advances existing orders through the bar’s price path, delivers fills and cancellations, updates account state, calls the strategy, and submits its orders. Ordinarily an order decided on bar N first evaluates on the next bar of its symbol. Market-on-close is an explicit exception: it settles at the already-observed close on submission. Protective children activate when their parent fills, so they can act within that same bar. Under --lifetime v2 the same bar adds, after the submissions, the engine’s answers (an OrderUpdate per submission, a CancelResponse per cancel request) and the lifecycle turns they grant, all before the clock advances; outcomes deferred at an instant are delivered at the top of the next bar, before its rejection cancels and venue events, or before the shutdown sweep at the end of data (Lifecycle turns).

The SDK applies fills to position state even when a strategy has no OnFill handler. ctx.Trade, ctx.Position.Realized, Position.OpenedAt, automatic client IDs, ctx.PrimarySymbol, ctx.IsPrimary and ctx.BarCount are implemented SDK facilities. ctx.Cancel carries explicit cancellation requests over the strategy wire. StopTouch lifts the ordinary stop order’s wrong-side submission guard; once resting it uses the same fill rules.

At shutdown, working orders are cancelled before the strategy receives its shutdown event. Stdout belongs to the wire; diagnostics use stderr or SDK logs. --lifecycle-json records this sequence, bar by bar, as the order lifecycle trace.

Reporting calculations

engine/report.Recorder observes events without submitting orders or changing execution. It pairs opposite fills FIFO, splits a through-zero reversal into a close and a new entry, and retains open lots separately from closed-trade statistics. Continuous stitch mode translates roll events into one economic trade; split mode retains per-contract legs.

Gross trade P&L includes the effect of execution-price slippage. Commission is deducted separately; slippage is disclosed, not deducted twice. Win/loss classification uses gross P&L. Headline net profit is final marked equity minus starting capital. The pairing residual is closed gross P&L + open P&L - commission - net profit, exposing rather than hiding differences between trade pairing and account equity.

Daily statistics use the last observation on each UTC date. Sharpe uses sample standard deviation, 252-day annualisation and at least 20 returns; unavailable ratios render as n/a instead of invented zeroes. Headline curve metrics use marked equity. All/Long/Short statistics use the corresponding gross closed-trade curves. The report also includes drawdowns, CAGR, Sortino, Calmar, linear fit, Ulcer index, monthly P&L and time in market.

Portfolio combination commits all constituent capital from portfolio start, forward-fills between observations, and retains the final equity after a run ends. It recomputes statistics from the combined curve. Benchmark reference data is isolated from strategy and simulator inputs. Only the primary benchmark has an equity-CSV column and chart curve; filtered portfolio pages show non-primary comparisons as unavailable until the whole set is selected.

See engine/report/types.go for persisted types, trades.go for pairing, metrics.go, stats.go and curve.go for formulas, combine.go for portfolio rules, and the accompanying tests for numerical examples.