ores.marketdata.market_series

Table of Contents

A catalog entry for a market data series — it records what is being observed: a yield curve, vol surface, spot rate, fixing index, or similar. Standard temporal reference data; changes infrequently so a regular table with GIST exclusion is appropriate.

The series is identified by oresmd_uri, the series URI of the datum its ORE key names, or the fixing URI of its index name. The URI holds every field of the series' identity, so the table keeps no decomposed copy of the key; series_subclass carries the coarse taxonomy for filtering, and the asset classes the series belongs to live in market_series_asset_classes – a set rather than a column, because a pairwise correlation relates two classes at once.

derivation_kind=/=derivation_config_id=/=derivation_config_version mark whether this series is directly observed (the sentinel OBSERVED) or derived by a named mechanism (e.g. IR_CURVE_BOOTSTRAP, CRM_DERIVATION) – so any published series answers "was this observed or computed, and by what" without guessing from the source tag on its observations. Per-observation lineage (which source series/as-of a specific derived point came from) is a separate concern, tracked by observation_lineage, not this catalog-level marker.

provider_code names the provider whose ids the series carries. A series is one item of one provider held by one party, so two providers that quote the same item keep two series, and a party that switches the source of a provider keeps one series and one history. The observations beneath it record the source that produced each one, for information; the source is never part of the key.

1. Flags

2. Columns

2.1. id

Surrogate UUID uniquely identifying this market series.

2.2. party_id

Party that owns this market series.

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

ctx.generate_uuid()

2.3. oresmd_uri

The oresmd identifier this series is, written as a URI: the canonical form of the key's series part – the key without the point the observation at hand carries – or the index name the row arrived under, and the identity the series is read by. The point belongs to the observation rows beneath this one, so a series with two points carries one identity rather than the identity of whichever point arrived first.

The natural key with party_id and provider_code, and unique per party and provider: one party holds one series per identity and provider. It is the only name the row carries: the registry's series_type, metric and qualifier columns have gone, and a reader that wants the key's parts reads the identity through oresmd_uri_codec rather than off a series.

Every writer supplies one. A caller that leaves it empty stores nothing, because the column is not null and the generated service copies the write's fields straight through – so an update carries the identity it read rather than clearing it.

The test-data generator writes a generic index name, the one oresmd URI that names a series with nothing but a name. Its rows cannot collide on the identity: the party is freshly generated per row, and the party is part of the key.

std::string("oresmd://generic/") + std::string(faker::word::noun()) + "-" +
    std::to_string(idx) + "?type=fixing"

2.4. series_subclass

Subclass within the asset class, as a code from refdata.series_subclass_code (referenced series_subclass_code.code). Validated by ores_refdata_validate_series_subclass_code_fn, the same cross-table reference mechanism the asset-class junction uses, and carried as the code itself: the taxonomy is runtime-managed.

[idx]{
    static constexpr std::string_view subclasses[] = {
        "spot",    "forward", "volatility", "yield",       "basis",
        "fra",     "xccy",    "spread",     "index_credit", "recovery",
        "swap",    "capfloor", "seasonality", "price",     "correlation"};
    return std::string(subclasses[static_cast<std::size_t>(idx) % std::size(subclasses)]);
}()

2.5. provider_code

References provider.code: the provider whose ids the series carries. The natural key with party_id and oresmd_uri: one party holds one series of each provider for each item.

std::string("ore")

2.6. derivation_kind

References derivation_kind.code – whether this series is directly observed (the sentinel OBSERVED, the default) or derived by a named mechanism (IR_CURVE_BOOTSTRAP, CRM_DERIVATION, …). Paired with derivation_config_id=/=derivation_config_version: a row's own derivation_kind is the discriminator that says whether those two fields are meaningful or hold their nil/zero sentinel.

std::string("OBSERVED")

2.7. derivation_config_id

The recipe/config that produced this series, when derived – ores_utility_nil_uuid_fn() (the sentinel, not null) when derivation_kind is 'OBSERVED'. Deliberately not a hard FK: the table this id resolves against depends on derivation_kind (the IR curve bootstrap config for IR_CURVE_BOOTSTRAP, the CRM topology config for CRM_DERIVATION), so it is a soft, self-describing reference.

boost::uuids::nil_uuid()

2.8. derivation_config_version

The derivation config's version this series was produced under – 0 (the sentinel) when derivation_kind is 'OBSERVED'.

0

3. SQL

3.1. Flags

3.2. Checks

expression
"oresmd_uri" <> ''
"series_subclass" <> ''
("derivation_kind" = 'OBSERVED' and "derivation_config_id" = ores_utility_nil_uuid_fn() and "derivation_config_version" = 0) or ("derivation_kind" <> 'OBSERVED' and "derivation_config_id" <> ores_utility_nil_uuid_fn() and "derivation_config_version" <> 0)

4. Insert trigger

4.1. Validations

column validation_function
series_subclass ores_refdata_validate_series_subclass_code_fn
provider_code ores_marketdata_validate_provider_fn
derivation_kind ores_refdata_validate_derivation_kind_fn

5. C++

5.1. Flags

5.2. Repository

5.2.1. read_latest_by_key

Reads the latest market series matching oresmd_uri and provider_code. When party_id is non-empty, scopes the result to that party.

The URI and the provider are the identity, so a reader that starts here finds a series without knowing how the registry decomposed its key. The method has no default for the provider: a caller that does not know it cannot name a series.

std::vector<domain::market_series> read_latest_by_key(context ctx,
    const std::string& oresmd_uri, const std::string& provider_code,
    const std::string& party_id = {});
#include <string>
std::vector<domain::market_series>
market_series_repository::read_latest_by_key(context ctx, const std::string& oresmd_uri,
                                             const std::string& provider_code,
                                             const std::string& party_id) {
    static constexpr auto max = db_timestamp::max();
    const auto tid = ctx.tenant_id().to_string();

    if (!party_id.empty()) {
        const auto query = query_builder::read<std::vector<market_series_entity>> |
                           query_builder::where("tenant_id"_c == tid && "party_id"_c == party_id &&
                                 "valid_to"_c == max &&
                                 "provider_code"_c == provider_code &&
                                 "oresmd_uri"_c == oresmd_uri) |
                           query_builder::order_by("id"_c);
        return query_builder::execute_read_query<market_series_entity, domain::market_series>(
            ctx, query,
            [](const auto& entities) { return market_series_mapper::map(entities); },
            lg(), "Reading latest market series by URI, provider and party");
    }

    const auto query = query_builder::read<std::vector<market_series_entity>> |
                       query_builder::where("tenant_id"_c == tid && "valid_to"_c == max &&
                             "provider_code"_c == provider_code &&
                             "oresmd_uri"_c == oresmd_uri) |
                       query_builder::order_by("id"_c);
    return query_builder::execute_read_query<market_series_entity, domain::market_series>(
        ctx, query,
        [](const auto& entities) { return market_series_mapper::map(entities); },
        lg(), "Reading latest market series by URI and provider");
}

5.2.2. read_latest_by_uri

Reads the latest market series of oresmd_uri from every provider. A reader that wants a value and does not care who supplied it, such as the spot of an FX forward, has no provider to name.

std::vector<domain::market_series> read_latest_by_uri(context ctx,
    const std::string& oresmd_uri);
std::vector<domain::market_series>
market_series_repository::read_latest_by_uri(context ctx, const std::string& oresmd_uri) {
    static constexpr auto max = db_timestamp::max();
    const auto tid = ctx.tenant_id().to_string();
    const auto query = query_builder::read<std::vector<market_series_entity>> |
                       query_builder::where("tenant_id"_c == tid && "valid_to"_c == max &&
                             "oresmd_uri"_c == oresmd_uri) |
                       query_builder::order_by("id"_c);
    return query_builder::execute_read_query<market_series_entity, domain::market_series>(
        ctx, query,
        [](const auto& entities) { return market_series_mapper::map(entities); },
        lg(), "Reading latest market series by URI");
}

5.2.3. project_identity_after_write

Projects the identity of every series the generated write path writes, so a series is never committed without the row a query joins on.

The hook sits where the row is written rather than at each call site, because every writer of a series reaches this method: the ingest loop, the curve republish, the import and the service. The projector reads the URI through the codec, so the projection cannot disagree with the identity the ticks land under, and the write joins the transaction the context already carries.

A repository of another entity collapses these markers, so the hook costs every other entity nothing.

#include "ores.marketdata.core/repository/market_series_identity_projector.hpp"
market_series_identity_projector::project(ctx, std::vector<domain::market_series>{t});
market_series_identity_projector::project(ctx, batch);

5.3. Handler: backfill_series_identity

Handles backfill_series_identity_request: projects the identity of every series the tenant holds that has none, so a series written before the projection existed becomes joinable.

This is the repair for rows the write path never saw, a database restored from a dump or a series written before the projection landed. It reads what is already projected rather than re-projecting everything, so a second call finds nothing to do and says so. The work is the projector's project_missing, which the data quality publish handler also calls.

#include "ores.marketdata.core/repository/market_series_identity_projector.hpp"
#include "ores.marketdata.core/repository/market_series_identity_repository.hpp"
#include "ores.marketdata.core/repository/market_series_repository.hpp"
#include <boost/uuid/string_generator.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <optional>
#include <set>
#include <string>
#include <vector>
void backfill_identity(ores::nats::message msg) {
    BOOST_LOG_SEV(market_series_handler_lg(), debug) << "Handling " << msg.subject;
    auto req_ctx_expected = ores::service::service::make_request_context(ctx_, msg, verifier_);
    if (!req_ctx_expected) {
        error_reply(nats_, msg, req_ctx_expected.error());
        return;
    }
    const auto& req_ctx = *req_ctx_expected;
    if (!has_permission(req_ctx, "marketdata::market_series:write")) {
        error_reply(nats_, msg, ores::service::error_code::forbidden);
        return;
    }
    auto req = decode<backfill_series_identity_request>(msg);
    if (!req) {
        BOOST_LOG_SEV(market_series_handler_lg(), warn) << "Failed to decode: " << msg.subject;
        error_reply(nats_, msg, ores::service::error_code::bad_request);
        return;
    }

    backfill_series_identity_response resp;
    try {
        std::optional<boost::uuids::uuid> party;
        if (!req->party_id.empty())
            party = boost::uuids::string_generator{}(req->party_id);

        const auto done =
            repository::market_series_identity_projector::project_missing(req_ctx, party);
        const auto total = done.projected + done.already_projected;

        resp.projected_count = static_cast<int>(done.projected);
        resp.already_projected_count = static_cast<int>(done.already_projected);
        resp.unreadable_count = static_cast<int>(done.unreadable);
        resp.success = true;
        resp.message = "Projected " + std::to_string(done.projected) + " of " +
                       std::to_string(total) + " series, " + std::to_string(done.unreadable) +
                       " with an unreadable URI.";
        BOOST_LOG_SEV(market_series_handler_lg(), info) << msg.subject << ": " << resp.message;
    } catch (const std::exception& e) {
        BOOST_LOG_SEV(market_series_handler_lg(), error)
            << msg.subject << " failed: " << e.what();
        resp.message = e.what();
    }
    reply(nats_, msg, resp);
}

5.4. Domain includes

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

5.5. Entity includes

#include <string>

5.6. Handler: export_to_storage

Handles export_market_data_to_storage_request: writes the tenant's market data to object storage as the ORE text an engine run reads, and returns the storage key. Used by the reporting workflow to gather market data for report generation. Not part of the CRUD surface: its messages are declared by ores.marketdata.operations and arrive with the component's generated operations_protocol.hpp.

The engine is handed a run document that names a market data file, and it reads that file and nothing else, so what is stored here is that body. A store of the series rows would be a different kind of thing and the engine could not read it.

#include "ores.marketdata.api/messaging/operations_protocol.hpp"
#include "ores.marketdata.core/messaging/party_required.hpp"
#include "ores.marketdata.core/service/ore_export_service.hpp"
#include "ores.nats/service/nats_client.hpp"
#include "ores.storage.core/net/storage_transfer.hpp"
#include <chrono>
void export_to_storage(ores::nats::message msg, const std::string& http_base_url) {
    [[maybe_unused]] const auto correlation_id =
        ores::service::messaging::log_handler_entry(market_series_handler_lg(), msg);
    auto ctx_expected = ores::service::service::make_request_context(ctx_, msg, verifier_);
    if (!ctx_expected) {
        error_reply(nats_, msg, ctx_expected.error());
        return;
    }
    const auto& ctx = *ctx_expected;
    export_market_data_to_storage_response resp;
    try {
        auto req = decode<export_market_data_to_storage_request>(msg);
        if (!req) {
            resp.message = "Failed to decode request.";
            reply(nats_, msg, resp);
            return;
        }
        if (!names_a_party(ctx)) {
            resp.message = party_required_message();
            reply(nats_, msg, resp);
            return;
        }

        const auto began = std::chrono::steady_clock::now();
        const auto since = [&began] {
            return std::chrono::duration_cast<std::chrono::milliseconds>(
                       std::chrono::steady_clock::now() - began)
                .count();
        };
        BOOST_LOG_SEV(market_series_handler_lg(), info)
            << "export_to_storage: exporting for valuation date '"
            << req->valuation_date.value_or("") << "' to " << req->storage_bucket << "/"
            << req->storage_key;

        service::ore_export_service svc(ctx);
        const auto written = svc.write_all(req->valuation_date, req->max_age_days);
        const auto exported_ms = since();

        ores::storage::net::storage_transfer transfer(http_base_url,
                                                      ores::nats::service::extract_bearer(msg));
        transfer.upload_blob(req->storage_bucket, req->storage_key, written.market_data);
        transfer.upload_blob(req->storage_bucket, req->fixings_storage_key, written.fixings);
        BOOST_LOG_SEV(market_series_handler_lg(), info)
            << "export_to_storage: built the export in " << exported_ms
            << " ms and uploaded it in " << (since() - exported_ms) << " ms";

        resp.success = true;
        resp.series_count = written.series_count;
        resp.carried_forward_count = written.carried_forward_count;
        resp.stale_count = written.stale_count;
        resp.storage_key = req->storage_key;
        resp.fixings_storage_key = req->fixings_storage_key;
        resp.message = "Exported " + std::to_string(written.series_count) +
                       " series as ORE market data to storage.";

        BOOST_LOG_SEV(market_series_handler_lg(), info)
            << "export_to_storage: wrote " << written.market_data.size()
            << " bytes of ORE market data for " << written.series_count << " series ("
            << written.observation_count << " observations, " << written.fixing_count
            << " fixings) to " << req->storage_bucket << "/" << req->storage_key;
    } catch (const std::exception& e) {
        BOOST_LOG_SEV(market_series_handler_lg(), error)
            << msg.subject << " failed: " << e.what();
        resp.message = e.what();
    }
    reply(nats_, msg, resp);
}

5.7. Conventions

Emacs 29.3 (Org mode 9.6.15)