Snapshots and cuts

Table of Contents

1. Summary

Three read-only operations answer questions about observations that are already stored. marketdata.v1.curve_snapshot.get returns the latest value of every node of one composite object, as of now. marketdata.v1.ops.get_series_slice returns one component of a composite object at each instant in a range. marketdata.v1.ops.get_series_evolution returns the whole object as a matrix of instants by nodes, with the change each node shows.

The snapshot is served by curve_snapshot_handler, the slice by series_slice_handler and the evolution by series_evolution_handler, all in projects/ores.marketdata/core/include/ores.marketdata.core/messaging/. Each handler calls a reader in ores.marketdata.core/service/: series_snapshot_reader, series_slice_reader and series_evolution_reader.

2. Detail

2.1. The snapshot

get_curve_snapshot_request carries one field, the typed identity the resolver takes, as the slice and the evolution requests do. A caller never builds an oresmd URI and never handles an internal series id. The oresmd datum identity stays the key of the series underneath.

series_snapshot_reader resolves the series, reads its declared shape and builds the grid with series_grid, the same grid the evolution reader builds. It then calls market_observation_repository::read_as_of_records with the handler's current time. It places each point at its node with the same series_grid::node_of the evolution uses. The response states the axis names and the nodes once, then one value and one record time per node. A node that no point states is a hole: the empty string and the epoch. An evolution returns the same axes and the same nodes. The two differ in one way. The snapshot carries the latest value of a node forward from before the instant, and the evolution states only what an instant states. Where the evolution holds a value, the snapshot holds the same one, and a test checks it.

A caller that sets include_provenance also gets, for each node, where its value came from. The source kind is quoted for a point with no annex row, and derived or manual as the annex row of observation_lineage states. A derived or manual point also carries who set it, why and when the annex row was recorded. A derived point also names the recipe that built it: the derivation config, its version and the as-of of the data the recipe read. These three are empty, zero and the epoch for every other point. A hole has no source, so its entry is empty. A client tests source_kind to tell a hole from a point and never tests recorded_at, which is the epoch for a quoted point as well as for a hole. The provenance is the current annex row of each point shown, the same generation the values come from, and not the row as it stood at the instant. The reader asks the annex only for the points it placed, and only when the caller asks, so the point path pays nothing for provenance. The annex is the one cold table keyed by the point's natural key (series, observation instant and oresmd URI).

The staleness figures (oldest age, spread and the warning) are measured over every point the read returns.

An object that declares no axis is refused, as it is for the slice and the evolution. A series that does not exist yet is not an error. A feed that has not published leaves an empty snapshot with success = true, because an empty curve is a normal state and not a failure.

The as-of time is always now. There is no parameter for an instant in the past: the evolution read answers that question with a stated set of instants.

2.2. The slice and the evolution

Both name the object by the typed identity fields the resolver takes, so no caller builds an oresmd URI, and both take a range or a set of instants. A set the caller states replaces the range.

get_series_slice_request adds the component: an axis and a value the shape declares. The answer holds the declared values of the other axis once, in the order the shape stores them, and one row per instant whose values are index for index with them. An object of one axis has one component, the whole object, and names no component field. An object of more than two axes is refused, because a term structure has one ladder.

get_series_evolution_request takes no component. The answer holds every combination of the declared axis values, with the last axis varying fastest, and one row per instant. Each node carries the change it shows over the range: never_quoted when no instant states it, persistent when every instant does, added when the last does and the first does not, dropped when the first does and the last does not, and intermittent otherwise.

2.3. An instant's points are the ones it states

market_observation_repository::read_instants returns the points a series states at each instant, and not the ones it held by then. A point a later build does not state is absent from that build rather than carried forward from the last time it was stated. That is what lets a reader see a node a structure gained or lost, which is the shape change the slice and evolution reads exist to report.

A reader that wants the object as it stood at an instant, assembled from whatever the last quote per coordinate was, asks market_observation_repository::read_as_of_records or read_as_of_buckets instead. The snapshot read above is the first of those. read_as_of_buckets remains the repository's as-of read over a uniform grid; the wire window that used it, marketdata.v1.ops.get_curve_snapshot_buckets with its fixed five half-hour buckets ending now, is gone.

2.4. These reads take ticks and observations side by side

read_as_of_records, read_as_of_buckets and read_instants read the tick table with the observation table, and so does the ORE export. For a coordinate they take the latest tick at or before the bound. A tick has no source and no id, so the read gives it an id that it builds from the series, the coordinate and the time, and a record time equal to its own time. When a tick and an observation state the same coordinate at the same instant, the observation wins.

A context with no party sees no ticks, because the party policy of the tick table lists the parties a session may see. A snapshot, a slice, an evolution or an export that lacks its live values for that reason must not look complete. So the handler of each one refuses a session that names no party. The reply has success false and the sentence of the outcome party_required, which the history call reports in the same words. The readers do not check, because a reader takes the context its caller gives it, and a test can read the observations of a tenant through a tenant context. A report run reads with a run token that names one party, so it reads the ticks of that party and no others.

These reads value a series at a time. The history of ticks is another question, and The history call returns every tick of a range in one reply, or refuses the range answers it.

2.5. The instant ceiling

Both range reads declare max_instants = 200 as a server side ceiling, enforced whatever a caller asks for. A range that holds more instants is refused, naming the ceiling, rather than truncated: a caller that asked for a quarter and silently received a day would read the answer as complete. The reason is cost. An oversized range buys a large join and a matching result allocation.

2.6. How a malformed read is refused

projects/ores.marketdata/core/include/ores.marketdata.core/repository/as_of_rows.hpp decodes the raw rows the database returns. as_of_observation reads the ten observation columns from a given offset, and throws when the row is too short to carry them. as_of_bucket_ordinal reads the bucket ordinal from the first column, and throws when the ordinal is missing, is not all digits, or is not below the bucket count.

The throw is deliberate. A short row would leave a curve snapshot one pillar short with no visible sign, so the read fails instead of returning a partial curve.

2.7. How this differs from the resolution-side snapshot

Market Data Snapshot is the resolution-side concept, a requirement satisfied from identifiers. This note is about the stored data, a read of rows that already exist. The two share a word and nothing else.

A cut is a third thing and is neither of these. It is a label for a timepoint and the object it fixes, and the object is everything the valuation read, including the reference data that gives the deals and the quotes their meaning. The as-of read above is one of the pieces such a cut would need, and not a cut itself. See Cuts.

2.8. Facts the code shape does not show

  • The snapshot is always taken at the service's own clock: its request carries no as-of field and the handler passes std::chrono::system_clock::now(). The slice and the evolution carry their range in the request, so both can ask for the store as it stood at a past instant.
  • Each of the three reads owns its own permission check, all of them marketdata::curve_snapshots:read, because they read the same data.
  • The handler owns the series id lookup, so the reads are the only read path that needs no series registry knowledge from the client.
  • Every request declares requires_session = true, so a caller establishes a session first.

3. See also

  • Market Data Architecture — the structure note that orders this cluster, and where to read this page in it.
  • Market Data Snapshot, the resolution-side snapshot this note is not about.
  • Cuts, the structure note for the cluster: a cut is a label for a timepoint and the object it fixes, and this read is one piece of such an object.
  • Series and observation, what the reads return rows from.
  • The history call returns every tick of a range in one reply, or refuses the range, the read of tick history that these reads are not.
  • The handlers are curve_snapshot_handler, series_slice_handler and series_evolution_handler in projects/ores.marketdata/core/include/ores.marketdata.core/messaging/.
  • The row helpers are projects/ores.marketdata/core/include/ores.marketdata.core/repository/as_of_rows.hpp.

Emacs 29.3 (Org mode 9.6.15)