ores.marketdata.curve_publish
Table of Contents
The detail row of one marketdata.curve_publish approval request: the subject
the request authorises, and the record of the act once the subject is published.
A request is a header. What it is about lives here, keyed by the request's own
id, exactly as a proposed book change lives in ores.refdata.book_change and is
keyed by its request. The subject is three facts and no more: the curve
configuration whose output is published, the as-of instant the bootstrap reads
the market at, and the party the configuration belongs to (party_id). The curve
is named by its configuration because that is what the publish call names. The
series the output is written to (output_series_id) is carried beside it because
the reader that serves a published curve starts from a series and not from a
configuration.
published_at and published_by are the publication record. Both are empty
while the request waits, and both are written in one act when the approved
subject is published: the timestamp is the instant the published set moved, and
the username is the account that applied it. The row is bitemporal, so the
applied version is a new version of the same row and the history holds the
unapplied subject beside it. request_id is the natural key, so a request
authorises one publish and no second apply can find a row to write.
A set ends once, and the end is two more facts on the same row rather than a
second entity. withdrawn_at, withdrawn_by, withdrawn_reason_code and
withdrawal_request_id are the withdrawal: the instant, the account, the reason
and the request that authorised it, written as a new version of this row. The
withdrawal is not a deletion: the published facts stay, so the set a desk priced
from is still readable afterwards. superseded_at,
superseded_by_request_id and superseded_as_of are the supersession: a later
publish replaced this set, and the link names the successor's request and the
as-of it was built at. A supersession is a publish and not a second act, so the
successor's own apply writes it on this row.
The state a reader sees is a function of these facts and is stored nowhere:
withdrawn when withdrawn_at holds an instant, superseded when superseded_at
does, published when published_at does, and a candidate otherwise.
The row is not a client's to write: the raise writes it, the gate reads it, and the apply stamps it. A client reads it to decide a request or to audit a curve.
1. Flags
2. Columns
2.1. id
Surrogate UUID identifying this publish record.
2.2. request_id
The approval request this record is the subject of.
One request, one publish: the row is written when the request is raised and stamped when it is applied, so a request that authorised one publish has no second row to authorise another.
ctx.generate_uuid()
2.3. party_id
The party the published configuration belongs to.
The subject names it because the same curve code in two parties is two curves, and a publish of one must not authorise the other.
ctx.generate_uuid()
2.4. bootstrap_config_id
The ir_curve_bootstrap_config whose output the publish writes.
The publish names the configuration and not a set of points, because the configuration is what the bootstrap recomputes from the market.
ctx.generate_uuid()
2.5. output_series_id
The market series the configuration's output is written to.
Carried beside the configuration so a reader that starts from a series – the snapshot read, a consumer of the curve – finds the publish record for the series it holds without resolving the configuration first.
ctx.generate_uuid()
2.6. as_of
The instant the bootstrap reads the market at.
The as-of is part of the subject: the same curve published against two market instants is two different published sets, and one request authorises one of them.
ctx.past_timepoint()
2.7. published_by
The username of the account that applied the request, or empty while it waits.
The account is the publisher, which is not the decider: the approval records who answered, and this records who made the published set move.
std::string("")
2.8. published_at
When the published set moved, or null while the request waits.
This is the published marker: a series is published when a record for it holds an instant here, and the latest such instant is the version in use.
std::nullopt
2.9. withdrawn_by
The username of the account that took the set out of use, or empty while the set is in use or ended by a successor.
The account is the withdrawer, which is not the publisher: the journey states that the publisher of a set has no special right to withdraw it, and the reason is the error and not the person.
std::string("")
2.10. withdrawn_at
When the set left use, or null while it is in use.
The instant is the end of the act, and it is written in the same version as the withdrawer and the reason, so a reader never sees a withdrawal with no account behind it.
std::nullopt
2.11. withdrawn_reason_code
Why the set left use, as a change reason code, or empty while it is in use.
The journey keeps the reason against the act, so the code travels with the instant and the account. The screen offers the reasons a curve leaves use and the record holds the one chosen.
std::string("")
2.12. withdrawal_request_id
The withdrawal request that authorised the act, or null.
Null is not a gap: a tenant whose withdrawal kind requires no approval lets the
trader withdraw alone, and the act then names no request. The column is a soft
reference to ores_inbox_approval_requests_tbl rather than a foreign key,
because the row states its own authority and a hard key would refuse the
count-zero path the journey allows.
std::nullopt
2.13. superseded_at
When a later set took this one's place, or null while this set is in use or withdrawn.
A supersession is a publish whose subject is the successor, so the instant is the successor's published instant and no person signs it: the successor's own record names the publisher.
std::nullopt
2.14. superseded_by_request_id
The publish request whose apply replaced this set, or null.
A soft reference, as the withdrawal request is: the successor is published under its own request and this row links to it so a reader follows one live set and one superseded set and never two live sets.
std::nullopt
2.15. superseded_as_of
The as-of the successor was built at, or null.
Carried beside the successor's request so the history names the set that replaced this one without a second read.
std::nullopt
3. SQL
3.1. Flags
The record is tenant-scoped and not party-scoped: the decider of a publish is a tenant function – Market Risk of the tenant – so hiding the subject behind the party a decider does not work in would hide the request's own subject from the person who decides it. The subject names its party instead, so a reader always knows whose curve it is.
3.2. Checks
| expression |
|---|
| ("published_at" is null) = ("published_by" = '') |
| "as_of" > '1970-01-01 00:00:00+00'::timestamp with time zone |
| ("withdrawn_at" is null) = ("withdrawn_by" = '') |
| ("withdrawn_at" is null) = ("withdrawn_reason_code" = '') |
| "withdrawn_at" is null or "withdrawn_at" >= "published_at" |
| ("superseded_at" is null) = ("superseded_by_request_id" is null) |
| ("superseded_at" is null) = ("superseded_as_of" is null) |
| ("withdrawn_at" is null) or ("superseded_at" is null) |
A set ends once, either because it was taken out of use or because a later set took its place. The last check states that, so the three end facts a reader finds are the three the state names.
3.3. Indexes
| name | columns | unique | current_only |
|---|---|---|---|
| curve_publishes_series_published | tenant_id, output_series_id, published_at desc | false | true |
| curve_publishes_withdrawal_request | tenant_id, withdrawal_request_id | true | true |
The second index is the single-use rule of a withdrawal: a decision authorises one act, so two sets cannot name the same withdrawal request. A set that ended without a request holds null there, and the store treats nulls as distinct, so every set a person ended alone is its own index entry.
4. Foreign keys
4.1. request_id
The request the record is the subject of. Its own id is the one the publish call names, so a record that names a request the store does not hold authorises nothing.
#include "ores.iam.api/generators/account_generator.hpp" #include "ores.iam.core/repository/account_repository.hpp" #include "ores.inbox.api/generators/approval_request_generator.hpp" #include "ores.inbox.core/repository/approval_kind_repository.hpp" #include "ores.inbox.core/repository/approval_request_repository.hpp" #include "ores.inbox.core/repository/approval_request_state_repository.hpp" #include "ores.utility/uuid/tenant_id.hpp"
{
auto asker = ores::iam::generators::generate_synthetic_account(ctx);
asker.account_type = "service";
asker.change_reason_code = "system.test";
ores::iam::repository::account_repository asker_repo;
asker_repo.write(party_ctx, asker);
auto request = ores::inbox::generators::generate_synthetic_approval_request(ctx);
request.change_reason_code = "system.test";
request.requested_by = asker.id;
const auto system_ctx =
party_ctx.with_tenant(ores::utility::uuid::tenant_id::system(), h.db_user());
const auto kinds =
ores::inbox::repository::approval_kind_repository{}.read_latest(system_ctx);
REQUIRE_FALSE(kinds.empty());
request.kind_code = kinds.front().code;
const auto states =
ores::inbox::repository::approval_request_state_repository{}.read_latest(system_ctx);
REQUIRE_FALSE(states.empty());
request.state_code = states.front().code;
ores::inbox::repository::approval_request_repository request_repo;
request_repo.write(party_ctx, request);
v.request_id = request.id;
}
4.2. party_id
4.3. bootstrap_config_id
4.4. output_series_id
5. C++
5.1. Flags
5.2. Repository
5.2.1. read_latest_by_request
Reads the record of one request, if the store holds one. The gate asks here: the subject the request authorises and whether it has been applied are one row.
std::optional<domain::curve_publish> read_latest_by_request(context ctx, const std::string& request_id);
#include <optional> #include <string>
std::optional<domain::curve_publish> curve_publish_repository::read_latest_by_request(context ctx, const std::string& request_id) { BOOST_LOG_SEV(lg(), debug) << "Reading the publish record of request: " << request_id; static constexpr auto max = db_timestamp::max(); const auto tid = ctx.tenant_id().to_string(); const auto query = query_builder::read<std::vector<curve_publish_entity>> | query_builder::where("tenant_id"_c == tid && "request_id"_c == request_id && "valid_to"_c == max); auto rows = query_builder::execute_read_query<curve_publish_entity, domain::curve_publish>( ctx, query, [](const auto& entities) { return curve_publish_mapper::map(entities); }, lg(), "Reading the publish record of a request."); if (rows.empty()) return std::nullopt; return rows.front(); }
5.2.2. read_latest_published_for_series
Reads the newest applied record of one series, which is the published set the readers serve. Empty when the series has never been published.
std::optional<domain::curve_publish> read_latest_published_for_series(context ctx, const boost::uuids::uuid& series_id);
#include <algorithm> #include <optional> #include <string> #include <boost/uuid/uuid_io.hpp>
std::optional<domain::curve_publish> curve_publish_repository::read_latest_published_for_series( context ctx, const boost::uuids::uuid& series_id) { BOOST_LOG_SEV(lg(), debug) << "Reading the published set of series: " << 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<curve_publish_entity>> | query_builder::where("tenant_id"_c == tid && "output_series_id"_c == sid && "valid_to"_c == max); auto rows = query_builder::execute_read_query<curve_publish_entity, domain::curve_publish>( ctx, query, [](const auto& entities) { return curve_publish_mapper::map(entities); }, lg(), "Reading the published set of a series."); // The newest applied version wins. A record that holds no instant is a // request that still waits, and a waiting request publishes nothing. rows.erase(std::remove_if(rows.begin(), rows.end(), [](const auto& r) { return !r.published_at.has_value(); }), rows.end()); if (rows.empty()) return std::nullopt; const auto newest = std::max_element(rows.begin(), rows.end(), [](const auto& a, const auto& b) { return *a.published_at < *b.published_at; }); return *newest; }
5.2.3. read_applied_for_series
Every set of a series that a person applied, in no order. A reader at a past as-of needs the set in force then, which is not always the newest.
std::vector<domain::curve_publish> read_applied_for_series(context ctx, const boost::uuids::uuid& series_id);
std::vector<domain::curve_publish> curve_publish_repository::read_applied_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<curve_publish_entity>> | query_builder::where("tenant_id"_c == tid && "output_series_id"_c == sid && "valid_to"_c == max); auto rows = query_builder::execute_read_query<curve_publish_entity, domain::curve_publish>( ctx, query, [](const auto& entities) { return curve_publish_mapper::map(entities); }, lg(), "Reading the applied sets of a series."); std::erase_if(rows, [](const auto& r) { return !r.published_at.has_value(); }); return rows; }
5.2.4. read_live_for_series
Reads every record of one series that was published and has not ended, newest first. The publish gate asks here before it stamps the successor: one live set is what a reader is promised, so a series that holds more than one is a set that never ended, and the apply ends them all.
std::vector<domain::curve_publish> read_live_for_series(context ctx, const boost::uuids::uuid& series_id);
#include <algorithm> #include <string> #include <vector> #include <boost/uuid/uuid_io.hpp>
std::vector<domain::curve_publish> curve_publish_repository::read_live_for_series(context ctx, const boost::uuids::uuid& series_id) { BOOST_LOG_SEV(lg(), debug) << "Reading the live sets of series: " << 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<curve_publish_entity>> | query_builder::where("tenant_id"_c == tid && "output_series_id"_c == sid && "valid_to"_c == max); auto rows = query_builder::execute_read_query<curve_publish_entity, domain::curve_publish>( ctx, query, [](const auto& entities) { return curve_publish_mapper::map(entities); }, lg(), "Reading the live sets of a series."); // A set is live when it was published and nothing ended it: a record with no // instant is a request that still waits, and one that carries an end is a set // a reader no longer prices from. rows.erase(std::remove_if(rows.begin(), rows.end(), [](const auto& r) { return !r.published_at.has_value() || r.withdrawn_at.has_value() || r.superseded_at.has_value(); }), rows.end()); std::sort(rows.begin(), rows.end(), [](const auto& a, const auto& b) { return *a.published_at > *b.published_at; }); return rows; }
5.3. Domain includes
#include <chrono> #include <optional> #include <string> #include <boost/uuid/uuid.hpp>
5.4. Entity includes
#include <optional> #include <string>
5.5. Conventions
5.6. Table display
| column | header |
|---|---|
| bootstrap_config_id | Curve |
| as_of | As Of |
| published_by | Published By |
| published_at | Published At |
6. See also
- ores.marketdata — component group overview.
- ores.marketdata.curve_publish_operations — the raise and the gate that read and write this record.
- ores.inbox.approval_request — the header this record is the detail of.
- ores.refdata.ir_curve_bootstrap_config — the configuration the subject names.