ores.marketdata.observation_lineage

Table of Contents

Per-observation provenance for a bare market_observation row whose source is not the market's own quote: which derivation config/version produced it, and which upstream source series/as-of it read, or – for a point an operator keyed by hand – which kind of point it is, who keyed it and why. Written never for the common quoted case, and never as a column on market_observations_tbl itself (a TimescaleDB hypertable explicitly documented as carrying no audit columns because tick-level volumes make that impractical).

point_source_kind names the two kinds that carry an annex row: derived for a point a derivation created, and manual for one keyed by hand. Quoted is the absence of a row, so a quoted tick keeps writing one row to the hot hypertable and nothing else; the invariant this rests on is that a point a derivation created always has an annex row.

Deliberately generic, not curve-specific: this is what lets CRM's own derived-cross publishing (currently pull-only – see the CRM architecture decision to never broadcast the full derived set as ticks) reuse this table unchanged whenever it starts persisting, stamping derivation_kind to 'CRM_DERIVATION' exactly as the IR curve bootstrapper stamps it to 'IR_CURVE_BOOTSTRAP'.

Standard bitemporal entity (GIST exclusion, soft-update/delete triggers), not a hypertable: unlike market_observations, lineage rows are written only for the minority of observations that are derived, so tick-level volume concerns that justify market_observations's own hypertable/no-audit-columns treatment do not apply here. A rerun of a derivation over the same tenor/point natural key closes the prior generation's lineage row and inserts a new one – the same soft-update convention every other bitemporal entity in this codebase already gets, mirroring (not literally reusing) how market_observations itself handles a rerun.

1. Flags

2. Columns

2.1. id

Surrogate UUID uniquely identifying this lineage row.

2.2. party_id

Party that owns the observation this lineage row describes. Included in the natural key because market_observations_tbl itself carries party_id as part of its own current-row uniqueness index – omitting it here would leave this table unable to uniquely identify the observation it describes whenever a series isn't 1:1 with a single party.

Set server-side from the authenticated session. Enforced by RLS.

ctx.generate_uuid()

2.3. series_id

Reference to ores_marketdata_market_series_tbl(id) – the derived series this observation belongs to.

2.4. observation_datetime

The market_observation row's own observation_datetime this lineage row describes.

ctx.past_timepoint()

2.5. oresmd_uri

The datum's canonical oresmd URI, matching the market_observation row's own oresmd_uri. Both are not null; the empty string is this column's default, and the only writer in the tree sets a real pillar.

std::string("")

2.6. point_source_kind

Which kind of point this annex row describes: derived for a point a derivation created, manual for one keyed by hand. The two values are validated by a check constraint. A point with no annex row is quoted, which is the absence of a row rather than a third stored value.

std::string("derived")

2.7. derivation_config_id

The derivation config (soft reference; table depends on the owning market_series.derivation_kind) that produced this observation. Null for a manual point, which was not produced by any derivation.

ctx.generate_uuid()

2.8. derivation_config_version

The derivation config's version this observation was produced under.

faker::number::integer(1, 10)

2.9. source_as_of

The upstream source data's as-of timestamp this derivation read (e.g. the raw instrument grid's as-of for a curve bootstrap, or the oldest contributing driver tick's timestamp for a CRM triangulation). Null for a manual point, which read no upstream source.

ctx.past_timepoint()

2.10. source_series_ids

Serialised JSON array of the upstream market_series ids this observation was derived from (e.g. the raw series for a Funding curve bootstrap; the raw series plus the discount curve's own output series for a Projection curve bootstrap; the walked driver series for a CRM triangulation). Empty ([]) for a manual point, which read no upstream series; the check admits the empty array while still refusing a value that is not a JSON array.

std::string(R"(["00000000-0000-0000-0000-000000000001"])")

3. SQL

3.1. Flags

The top-level * Flags drawer's natural_keys_composite_name: party_series_obs_point overrides the default auto-concatenated natural-key index name. It was added when that default spelled the coordinate point_id and came to 62 characters before the observation_lineages_=/=_uniq_idx prefix and suffix, over PostgreSQL's 63-byte NAMEDATALEN limit. The column is now oresmd_uri and the default would fit, but the override stays so the index name does not change under a database that already carries it.

3.2. Checks

expression
jsonb_array_length("source_series_ids") >= 0
"point_source_kind" in ('derived', 'manual')

4. C++

4.1. Flags

4.2. Repository

4.2.1. read_latest_by_observation

Reads the current lineage row for one observation, identified by series_id, observation_datetime, and oresmd_uri – the "which config/version/source produced this" lookup the shared derivation-lineage design calls for, in one round trip.

std::optional<domain::observation_lineage> read_latest_by_observation(context ctx,
    const boost::uuids::uuid& series_id,
    std::chrono::system_clock::time_point observation_datetime,
    const std::string& oresmd_uri);
#include "ores.platform/time/datetime.hpp"
#include <boost/uuid/uuid_io.hpp>
#include <optional>
std::optional<domain::observation_lineage>
observation_lineage_repository::read_latest_by_observation(context ctx,
    const boost::uuids::uuid& series_id,
    std::chrono::system_clock::time_point observation_datetime,
    const std::string& oresmd_uri) {
    using ores::platform::time::datetime;
    static constexpr auto max = db_timestamp::max();
    const auto tid = ctx.tenant_id().to_string();
    const auto sid = boost::uuids::to_string(series_id);
    const auto odt_str = datetime::to_iso8601_utc(observation_datetime);

    const auto query = query_builder::read<std::vector<observation_lineage_entity>> |
                       query_builder::where("tenant_id"_c == tid && "series_id"_c == sid &&
                             "observation_datetime"_c == odt_str &&
                             "oresmd_uri"_c == oresmd_uri &&
                             "valid_to"_c == max) |
                       query_builder::order_by("id"_c);
    const auto results = query_builder::execute_read_query<observation_lineage_entity, domain::observation_lineage>(
        ctx, query,
        [](const auto& entities) { return observation_lineage_mapper::map(entities); },
        lg(), "Reading latest observation lineage by observation");
    if (results.empty())
        return std::nullopt;
    return results.front();
}

4.2.2. read_latest_for_points

Reads the current lineage rows of the points a caller states, and nothing else. A point with no row is quoted, so the answer is shorter than the request when any is. The read is bounded by the stated points, not by the series, which is what lets a snapshot ask for the provenance of the points it placed.

/**
 * @brief The natural key of a market observation within one series, which is the
 * point an annex row describes.
 */
struct point_key {
    std::chrono::system_clock::time_point observation_datetime;
    std::string oresmd_uri;
};

/**
 * @brief The current annex rows of the stated points, one per point that has one.
 *
 * A point with no row is quoted, so the answer is shorter than @p points when any
 * is. The query takes the stated URIs and the span of the stated instants, which
 * is a cross-product, and the rows are then narrowed to the exact points, so a
 * wide span can read more rows than it returns. The rows are the current ones,
 * the same generation the as-of observation read uses for its values.
 */
std::vector<domain::observation_lineage> read_latest_for_points(context ctx,
    const boost::uuids::uuid& series_id,
    const std::vector<point_key>& points);
#include <algorithm>
#include <set>
#include <string>
#include <utility>
#include <vector>
std::vector<domain::observation_lineage>
observation_lineage_repository::read_latest_for_points(context ctx,
    const boost::uuids::uuid& series_id,
    const std::vector<point_key>& points) {
    using ores::platform::time::datetime;
    if (points.empty())
        return {};
    static constexpr auto max = db_timestamp::max();
    const auto tid = ctx.tenant_id().to_string();
    const auto sid = boost::uuids::to_string(series_id);

    // The query is a per-column cross-product of the stated URIs and the stated
    // time span, so the rows it returns are narrowed to the exact points below.
    std::vector<std::string> uris;
    uris.reserve(points.size());
    auto first = points.front().observation_datetime;
    auto last = first;
    for (const auto& point : points) {
        uris.push_back(point.oresmd_uri);
        first = std::min(first, point.observation_datetime);
        last = std::max(last, point.observation_datetime);
    }
    std::ranges::sort(uris);
    uris.erase(std::unique(uris.begin(), uris.end()), uris.end());

    const auto query = query_builder::read<std::vector<observation_lineage_entity>> |
                       query_builder::where("tenant_id"_c == tid && "series_id"_c == sid &&
                             "oresmd_uri"_c.in(uris) &&
                             "observation_datetime"_c >= datetime::to_iso8601_utc(first) &&
                             "observation_datetime"_c <= datetime::to_iso8601_utc(last) &&
                             "valid_to"_c == max) |
                       query_builder::order_by("id"_c);
    const auto rows = query_builder::execute_read_query<observation_lineage_entity, domain::observation_lineage>(
        ctx, query,
        [](const auto& entities) { return observation_lineage_mapper::map(entities); },
        lg(), "Reading latest observation lineages for points");

    std::set<std::pair<std::string, std::chrono::system_clock::time_point>> stated;
    for (const auto& point : points)
        stated.emplace(point.oresmd_uri, point.observation_datetime);

    std::vector<domain::observation_lineage> result;
    for (const auto& row : rows)
        if (stated.contains({row.oresmd_uri, row.observation_datetime}))
            result.push_back(row);
    return result;
}

4.2.3. read_manual_for_series

Reads the current annex rows of one series that name a manual point. The annex is cold and holds only the derived and manual points, and the manual ones are the few an operator keyed by hand, so the read is small. A write path asks it which coordinates an operator owns before it writes an automatic value.

/**
 * @brief The current annex rows of the series that name a manual point.
 */
std::vector<domain::observation_lineage> read_manual_for_series(context ctx,
    const boost::uuids::uuid& series_id);
std::vector<domain::observation_lineage>
observation_lineage_repository::read_manual_for_series(context ctx,
    const boost::uuids::uuid& series_id) {
    static constexpr auto max = db_timestamp::max();
    const auto tid = ctx.tenant_id().to_string();
    const auto sid = boost::uuids::to_string(series_id);

    const auto query = query_builder::read<std::vector<observation_lineage_entity>> |
                       query_builder::where("tenant_id"_c == tid && "series_id"_c == sid &&
                             "point_source_kind"_c == std::string("manual") &&
                             "valid_to"_c == max) |
                       query_builder::order_by("id"_c);
    return query_builder::execute_read_query<observation_lineage_entity, domain::observation_lineage>(
        ctx, query,
        [](const auto& entities) { return observation_lineage_mapper::map(entities); },
        lg(), "Reading the manual annex rows of a series");
}

4.3. Domain includes

#include <boost/uuid/uuid.hpp>
#include <string>

4.4. Entity includes

#include <string>

4.5. Conventions

5. See also

  • Market Series – carries the catalog-level derivation_kind marker this table's rows detail.
  • Market Observation – the table each lineage row describes one generation of.

Emacs 29.3 (Org mode 9.6.15)