Skip to content

Event groups

A venue lists its binary event contracts by event: one question, such as who wins an election or where an index closes, with one contract per answer. Each contract is an instrument with its own record. An event group says which of them belong to one event, and whether, as the venue states it, its members are mutually exclusive (at most one pays) and exhaustive (one of them must pay). A reader summing a group's prices, or the analytics component answering what an account makes if each outcome resolves, reads it.

An event group is reference data, kept in the instrument master beside the instruments it groups, not an observation in the lake. A venue's data plugin reports it, the platform matches or mints it, and every role that reads instruments reads it.

What a group holds

Part What
Its ID EVG- and a ULID, the platform's, never reused; or the deployment's own LCL- ID while it waits for the platform (see Reported, then minted).
The venue and its event The venue, by its venue ID, and the venue's own reference for the event: together, the group's identity.
Its title The venue's words for the event, data, never instructions.
Its members The deployment's instrument records of the event's contracts, 1 to 500.
Mutually exclusive, exhaustive Each EVENT_GROUP_FLAG_TRUE, EVENT_GROUP_FLAG_FALSE or EVENT_GROUP_FLAG_NOT_STATED.
Its series The venue's series, for a recurring event; empty where it has none.
Its version and valid time Each change a new version, from when it took effect.

Stated, or not stated

The two flags are separate, and each can be true without the other. A venue may say an event's markets are mutually exclusive without saying they cover every outcome; an event whose outcomes are named as they become known can be mutually exclusive and not exhaustive among those named so far.

Unknown is never false. A flag the venue does not state is reported NOT_STATED, and an unset one reads so. A reader counts a group as exhaustive only where the flag is TRUE: not stated is not. The dashboard says each flag as yes, no or not stated.

Every version kept

A group's members or flags change: an outcome is added, or a placeholder is named. Each change is a new version, valid from when it took effect, and every version is kept, so a reader computing over a group at a moment reads the version in force then. A report saying what the version in force already says keeps nothing new; a changed title or series alone is no new version.

Range bounds belong to the member

A range market, such as "between 4,000 and 4,100" or "5,000 or above", has its bounds on the member's own record, as the binary event contract template's fields, not on the group: a floor and a cap, each with whether it is inclusive, and their unit, since a bound is often a level, a temperature or a percentage rather than money. An open tail has one bound.

A venue's terms, stated at its resolve. A data plugin resolving a venue's event contract states its terms beside the identifiers, as the venue states them: its payout, close, bounds and category (the venue's own word, such as "Economics"), the payout left unstated where the venue gives none (resolve_identifier's stated_binary_event_contract; meridian.roles.data.event_contracts does it on SDK 0.36.0). The store records the category the first time it sees the contract, sourced to the plugin. Every other term, and a category unlike the one held, is offered, in force only when a person accepts it on the Instruments page. A reference plugin offers the template's fields as before.

Reported, then minted

A venue's data plugin reports the grouping its venue states with report_event_group: the venue, the event's reference and title, the members, each already resolved to the deployment's records, the two flags as stated, the series, when it took effect, and the raw record it was read from.

In the Python SDK, a venue of event contracts built on meridian.roles.data.event_contracts reads the venue's events into an Event and reports each with Feed.report_event. It resolves every member first, and holds the report back where one does not resolve: a group missing a member would misstate it. Each flag is True, False, or None where the venue says nothing, which is reported not stated; raw_key names the raw record the event was read from.

event = event_contracts.Event(
    ref="HIGHTEMP-NY-1101",
    title="Highest temperature in New York on 1 November",
    members=["HIGHTEMP-NY-1101-B60", "HIGHTEMP-NY-1101-B62", "HIGHTEMP-NY-1101-T64"],
    mutually_exclusive=True,
    exhaustive=None,              # the venue does not say: not stated
    raw_key=raw_key,
)
await feed.report_event(event)

The instrument store then keeps it:

  • Every member the platform's. Where every member has an INS- ID, the store asks the platform to match the group or mint it, and keeps it under its EVG- ID.
  • Otherwise, or while the platform cannot be reached, the store keeps the deployment's own group under an LCL- ID, the reply says so (pending_review where the platform holds it for review), and the next report asks again. A group with a member under a local record is never asked about.
  • Refused, naming the field, is a report with no venue, no event reference, no member or more than 500, a member the deployment does not hold (resolve it first), or a member named twice. A member replaced since it was resolved is followed to the record that replaced it.

Groups are not linked across venues: the same real-world event at two venues is two groups, since their rules for resolving it differ.

Reading groups

Every role that reads instruments, every role but pricing, reads groups with list_event_groups, by member or by group, at the version in force at a moment, and hears each new version, event_group_changed. meridian.EventGroups keeps them, caught up on start and after a gap, and answers the version in force at any moment:

groups = meridian.EventGroups()
await groups.catch_up(role)                       # in on_start, and after a gap
# and in the role class: async def on_event_group_changed(self, heard): groups.heard(heard)
for group in groups.holding("INS-...", at_ns=moment):
    if groups.mutually_exclusive(group) and groups.exhaustive(group):
        ...

Core records; it does not police. Nothing in core checks that exactly one member of a mutually exclusive and exhaustive group pays. That is a reader's check, an operations or signal plugin's, shown as a data problem. The book nets nothing across a group: a venue's collateral and charges reach the book as the venue reports them.

The analytics component reads a group for resolution_payoff and implied_distribution, at the version in force at the job's as-of, and names that version in the answer's provenance. A group not stated exhaustive answers its prices and their sum, its distribution not evaluated; a mutually exclusive one not stated exhaustive adds the outcome where none of the named members pays.

Where you see it

A member's page on the dashboard's Instruments page has an Event groups tab: each group's title, the venue's reference, its version, its flags as stated and its members, each with its payout and bounds, as the group stood at a date asked. Through an agent, dashboard__list_event_groups answers the same at the Instruments page's level.