Snapshots and cuts
Table of Contents
- 1. Summary
- 2. Detail
- 2.1. The snapshot
- 2.2. The slice and the evolution
- 2.3. An instant's points are the ones it states
- 2.4. These reads take ticks and observations side by side
- 2.5. The instant ceiling
- 2.6. How a malformed read is refused
- 2.7. How this differs from the resolution-side snapshot
- 2.8. Facts the code shape does not show
- 3. See also
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_handlerandseries_evolution_handlerinprojects/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.