Market events

Bars summarise the tape; sometimes a strategy wants the prints themselves. Declare a TRADES series and the engine delivers every trade of that instrument to the strategy between bars (GLE-389):

func (s *TapeReader) DataRequirements() []algolang.DataRequirement {
    return []algolang.DataRequirement{{
        SeriesID: "tape", Symbol: s.symbol, Lookback: s.Lookback,
        Schema:   algolang.DataSchemaSpec{Kind: algolang.Trades},
    }}
}

Each trade arrives as an algolang.MarketTrade:

FieldMeaning
SeriesIDthe declared series’ id (its Symbol when the declaration gave none)
Symbol, Price, Sizethe instrument, price and size of the print
AggressorAggressorBuy when the buyer lifted the offer, AggressorSell when the seller hit the bid, AggressorNone when the feed does not say
TsEventthe matching engine’s instant
TsRecvwhen the data provider received the trade: the instant it became visible
BadTsRecvthe provider flags TsRecv as unreliable

Read them two ways:

  • ctx.MarketTrades("tape") returns the newest Lookback trades of the series, oldest first, as a copy you own. A declaration without a Lookback keeps one trade. Each call copies the whole buffer, so size Lookback to what you read, or accumulate in OnMarketTrade instead.
  • The optional MarketTradeHandler interface, OnMarketTrade(ctx, t), is called once per trade in replay order, after the trade has joined the buffer and with ctx.Now() at its TsRecv. It is observe-only: it returns nothing, and orders still come only from OnBar.

Visibility. A trade becomes visible at its TsRecv. Before each bar the engine sends every trade received strictly before that bar’s close that it has not sent yet. A trade received at exactly a close is sent before the next bar, the same rule as a release stamped at a bar’s close: the bar [open, close) does not contain its close instant. So in OnBar the trades since the previous decision are those with prevClose <= TsRecv < bar.CloseTime. Several declared series merge into one replay: ascending TsRecv, ties in declaration order, then in the adapter’s order within a series.

Limits.

  • Declared trades are backtest-only; a live run refuses them.
  • The adapter must serve the TRADES schema. The Databento adapter does. marketfeed advertises TRADES for dated futures contracts, but the current archive holds no trade tapes (its futures are 1s/1m/1h/1d bars), so a declared tape there is refused with “no trades coverage”.
  • The strategy must speak the protobuf wire, which every declaring strategy does already.
  • The engine fetches each tape over the bar loop’s window, from the first bar’s open to the last bar’s close. There is no trade warm-up before the first bar, and the whole tape is held in memory for the run.
  • A trade series’ id must not be a bar series’ id. A declaration with no SeriesID takes its symbol, which is the id of that symbol’s configured bar series, so give the tape its own id.

Example. strategies/demo/go/tapereader reads the MSFT tape between one-minute bar closes. At each close it counts the trades since the previous close and splits their size by aggressor. With at least MinTrades trades and a net aggressor share (buy - sell) / (buy + sell) of at least Imbalance it wants a long of Qty shares, at most -Imbalance a short, and otherwise keeps what it holds. It reads its position from the engine and sends one market order for the difference.

$ make run-tapereader
[tapereader] INFO: tapereader: first trade tape MSFT price=265.76 size=14 recv=2022-06-10T12:30:01.100858345Z
[tapereader] INFO: tapereader: 2022-06-10T12:31:00Z trades=146 buy=260 sell=622 pos=0 target=-100
[tapereader] INFO: tapereader: 2022-06-10T12:32:00Z trades=68 buy=114 sell=417 pos=-100 target=-100
[tapereader] INFO: tapereader: 2022-06-10T12:33:00Z trades=70 buy=972 sell=1997 pos=-100 target=-100
[tapereader] INFO: tapereader: 2022-06-10T12:34:00Z trades=89 buy=1126 sell=211 pos=-100 target=100
...
[tapereader] INFO: tapereader: 2022-06-10T12:53:00Z trades=5 buy=0 sell=3 pos=-100 target=-100
  bars:       23 main after replicate (+ 0 warmup), 23 loop, 7 orders

The 23 windows hold all 1,000 trades of the sample. Trades with no aggressor (N) count in a window’s trades but in neither side, so buy + sell can be well below the window’s volume.

Statistics and definitions as events

A futures strategy can declare two more market-event kinds the same way (GLE-406): a contract’s venue statistics (Kind: algolang.Statistics: settlement price, open interest, cleared volume) and its point-in-time definitions (Kind: algolang.InstrumentDefinition: expiration, tick, maturity). They need an adapter that serves the schemas; marketfeed does (see Statistics and instrument definitions).

func (s *OIConfirm) DataRequirements() []algolang.DataRequirement {
    return []algolang.DataRequirement{
        {SeriesID: "stats", Symbol: s.symbol, Lookback: 400, Schema: algolang.DataSchemaSpec{Kind: algolang.Statistics}},
        {SeriesID: "defs", Symbol: s.symbol, Lookback: 1, Schema: algolang.DataSchemaSpec{Kind: algolang.InstrumentDefinition}},
    }
}

When they are visible. A statistic becomes visible when it was published: its TsRecv, else its TsEvent, else its TsRef (MarketStatistic.VisibleAt). A definition becomes visible at its TsEvent. Both then follow the trade rule: an event is sent before the first bar whose close is strictly after its visibility instant. A settlement for Monday’s session, published Monday evening, is therefore first seen by the bar that closes after it, never by an earlier one. The engine sorts each statistics series by visibility instant, because adapters serve statistics in trading-date order, not publication order. It fetches statistics from 10 days before the run’s first bar and definitions from 1 day before, so the latest settlement, open interest and definition are already known at the first bar.

Reading them.

CallReturns
ctx.MarketStatistics("stats")the newest Lookback statistics delivered, oldest first, as a copy
ctx.LatestStatistic("stats", sym, algolang.StatSettlementPrice)the latest of a contract and kind
ctx.Definition("defs", sym)the definition in force for a contract
ctx.Definitions("defs")every contract’s definition in force, sorted by symbol
OnStatistic(ctx, s) / OnDefinition(ctx, d)optional observers, once per event, with ctx.Now() at its visibility instant

A statistic is revised: one contract, kind and trading date can have several records published at different times, such as a preliminary and a final settlement, or an open interest restated the next morning. Each revision arrives as its own MarketStatistic. “Latest” means the record with the greatest trading date (TsRef), and among records for the same date, the last one delivered. A late correction of an older date never displaces a newer date’s value, and a deleted record (Deleted) never becomes the latest. A statistic the venue did not date (CME before 2015) has a zero TsRef, so its visibility instant stands in for its date. Venues resend definitions daily, and the definition in force is the last one delivered.

strategies/kit has pure helpers over ctx.MarketStatistics. OpenInterestChange gives the open-interest change between a contract’s two most recent trading dates, taking the latest revision of each date. SettlementGap gives a price minus the latest settlement. DaysToExpiry gives the days left before a definition’s expiration.

Example. strategies/demo/go/oiconfirm trades a dated ES contract’s daily bars:

  • It buys a close above the previous Breakout highs when open interest rose into the bar.
  • It exits when the close falls StopPoints below the latest settlement.
  • It flattens ExitDays before the contract’s expiration, which it reads from the definition in force.
$ make run-oiconfirm
[oiconfirm] INFO: oiconfirm: 2025-11-17T00:00:00Z close=6828.50 breakout=false oi_change=2360 oi_ok=true settle_gap=13.00 gap_ok=true days_left=123.6 pos=0 target=0
...
[oiconfirm] INFO: oiconfirm: 2025-11-27T00:00:00Z close=6888.25 breakout=true oi_change=5492 oi_ok=true settle_gap=2.00 gap_ok=true days_left=113.6 pos=0 target=1
...
[oiconfirm] INFO: oiconfirm: 2026-01-20T00:00:00Z close=6907.00 breakout=false oi_change=27426 oi_ok=true settle_gap=-69.75 gap_ok=true days_left=59.6 pos=1 target=0
...
[oiconfirm] INFO: oiconfirm: 2026-02-26T00:00:00Z close=6948.50 breakout=true oi_change=16627 oi_ok=true settle_gap=-11.25 gap_ok=true days_left=22.6 pos=0 target=1
...
[oiconfirm] INFO: oiconfirm: 2026-03-02T00:00:00Z close=6816.75 breakout=false oi_change=-29402 oi_ok=true settle_gap=-72.25 gap_ok=true days_left=18.6 pos=1 target=0
...
  bars:       101 main after replicate (+ 9 warmup), 101 loop, 4 orders
              4 fills, commission 0.00, realized PnL -5712.50

The pre-roll shows on the first bar: open interest, the settlement gap and the days to expiry are all known before the window’s first close. The daily bars close at 00:00 UTC, before the session settles, so the latest settlement a bar sees is the previous session’s. With Breakout=10 the window has five breakouts; two came while flat with open interest rising, and both positions left on the settlement-anchored exit (in January and again in March, 69.75 and 72.25 points below the latest settlement), before the expiry exit could arm. The example illustrates the API, not an edge.

Declared statistics and definitions are backtest-only and refused on a continuous run, like declared trades.