ores.refdata.ir_curve_bootstrap_config
Table of Contents
Records how a curve is bootstrapped as a named, inspectable
artefact – the "curve template" concept
Multicurve Management calls for. Owned by ores.refdata, matching
where every other recipe/config entity lives in this codebase
(crm_topology_config, curve_role, tenor), not ores.marketdata,
which owns only the generic value store the bootstrap output is
written into.
output_series_id is a soft, cross-component reference into
ores.marketdata's market_series table – the official curve series
this config publishes into – deliberately not a hard FK constraint,
matching the same soft-reference principle already used for
market_series.derivation_config_id: the referenced table lives in a different
component/schema. The caller names an existing market_series row when it
creates the config, and the column is never null – there is no "not yet
published" state to guard against.
It is chosen and not minted, decided 2026-10-28 when the blueprint screens were
built. The draft had the owning service mint the series from the config's own
fields, which it cannot: a curve's oresmd URI carries the index name
(DISCOUNT/RATE/USD/USD-SOFR-FOMC) and the config holds the currency and the
index tenor and not the index, so no operation can compose the identity the
record is keyed by. A desk therefore points a new recipe at a series it already
holds, which is also what makes the column usable as the sole natural key.
The config names no input series. Its pillars are the input: each pillar
carries the start and end tenor codes of the quote that fills it, and
curve_republish_service derives that quote's ORE key from the config's own
currency and the pillar's resolved dates, so a pillar is read from the series
the feed's tick for that pillar landed in. A raw-grid reference would be a
second spelling of the same input, with nothing keeping the two in step.
curve_family_role (FUNDING=/=PROJECTION) and
discount_curve_config_id (self-referencing, null for FUNDING) encode
Multi-Curve Construction's Funding-before-Projection build-order
dependency as data: a PROJECTION config's discount_curve_config_id
must point at a FUNDING config (strict two-tier, no chaining –
Multi-Curve Construction names basis-linked/cyclic Projection
dependencies as an explicit out-of-scope modelling gap, not something
this design should silently permit), and must not reference itself.
interpolation_method and curve_family_role are small,
fixed-vocabulary fields intrinsic to this record (not references to
another entity), following the same plain-check-constraint pattern
ir_curve_generation_config.role's 'self_discounting'=/=discount=/
=projection vocabulary already establishes – no lookup table for a
handful of values with no independent lifecycle of their own.
day_count_convention references the existing
day_count_fraction_type reference table. split_tenor_code
references tenor.code, following the same soft (undeclared,
documentation-only) tenor reference ir_curve_template_entry's own
start_tenor_code=/=end_tenor_code already use – tenor validation
happens through the tenor_resolution machinery at the application
layer, not a per-consumer DB-level FK, consistent with that sibling
entity. For a single-segment interpolation_method, split_tenor_code
is a genuine value (the curve's own last pillar's end_tenor_code),
not a fabricated sentinel – matching the spirit of
ir_curve_template_entries's own 'SPOT' tenor, which its own doc
comment is explicit isn't a sentinel hack either.
1. List contract
The curve blueprint screens list, search, filter and order these rows on the server, so the columns those reads need declare what they support:
nameis searchable and sortable: it is the record's display name, the handle a person knows a blueprint by, and the screens draw it in the list's first cell, in the record header and its breadcrumb, and as the first fact on Details. A blueprint is named in the database and is never named by parsing its output series' oresmd URI, so the screen has no name to derive and must not try.currency_codeandindex_tenorare searchable, so a person who knows the currency and the index it is quoted for finds a blueprint whose name they do not remember.currency_codeandcurve_family_roleare filterable, so the currency and role filters reach the server as filter members rather than narrowing a page the screen was given.currency_code,index_tenor,name,curve_family_role,interpolation_methodandday_count_conventionare sortable.discount_curve_config_idis filterable, so a funding blueprint can read the projection blueprints that discount off it without reading every config of the tenant.
2. Uniqueness
The name is unique per tenant and per party among the active rows: two of a
party's blueprints never carry one name, and one party's name does not spend
another party's. The model expresses that as a declared unique index,
ir_curve_bootstrap_configs_party_name_idx on
(tenant_id, party_id, name) where the row is current, because the natural-key
machinery emits one index per entity and that one already carries
=output_series_id – exactly one active config, tenant-wide, may produce any
given output series. The name is therefore a plain column and not a second
natural key: two natural keys would be emitted as one composite index over both
columns, which would drop the output-series rule and let two blueprints claim
one series as long as their names differ.
3. Flags
4. Columns
4.1. id
Surrogate UUID uniquely identifying this bootstrap config.
4.2. party_id
Owning party (legal entity) this bootstrap config belongs to. Set
server-side from the authenticated session, enforced by RLS.
Deliberately not part of the natural key below: uniqueness on
output_series_id is enforced tenant-wide, not per-party – two
different parties must not each be able to hold an active config
claiming the same output series.
ctx.generate_uuid()
4.3. name
The record's display name, as a person writes it: a short handle such as
USD-SOFR-ACME that tells one blueprint from another in a list, a breadcrumb
or a record header. It is typed by the caller and never derived: the web
browser does not parse the output series' oresmd URI for a name, and no
operation composes one from the config's own fields.
Unique per tenant and per party among the active rows, stated by the
party_name index below. It is the name and not the URI a person searches and
sorts by.
std::string("USD-SOFR-ACME")
4.4. currency_code
References currency.iso_code – the currency this curve's pillars are quoted in, and
the one segment of a pillar's ORE key the pillar itself cannot supply: a pillar carries
tenor codes and a role, so a reader resolving a pillar to its series has to be told the
currency the key is written in.
std::string("USD")
4.5. index_tenor
The tenor of the floating-rate index this curve stands for, as a period such as 1D for an
overnight index or 6M for a term index. It is the second segment a pillar cannot supply:
a pillar carries tenor codes and a role, and a series of the curve is named by the currency,
the pillar's start and the index tenor (the tenor identity field of the ir_swap series in
oresmd: ORE Studio Market Data URI). A reader
resolving a pillar to its series is told the tenor here, so a EURIBOR 3M curve and a EURIBOR
6M curve of one currency read two series and not one.
std::string("1D")
4.6. output_series_id
Soft reference to the official, bootstrapped-curve market_series
(in ores.marketdata) this config publishes into, named by the caller
from the series its desk already holds. The sole natural key: exactly
one active bootstrap config, tenant-wide, may produce any given
output series – not merely one per party, since two different
parties must not each be able to claim the same output series.
ctx.generate_uuid()
4.7. curve_family_role
Whether this config bootstraps the discounting anchor curve of its
family (FUNDING) or a dependent curve discounted off one
(PROJECTION) – see Funding and Projection Curves. Fixed 2-value
vocabulary intrinsic to this record, not a reference to another
entity – no soft FK, validated by a plain SQL check.
std::string("FUNDING")
4.8. discount_curve_config_id
Self-referencing: the FUNDING config this config's own pillars
discount off during bootstrap, when this config's own
curve_family_role is PROJECTION. It is null when the role is
FUNDING, because a funding config discounts off nothing: there is no
discount curve to name and no sentinel standing in for one. Strict two-tier
only (validated in the insert trigger, not just shape-checked): the referenced
config must itself have a curve_family_role of FUNDING, and must not
equal this config's own id. No code compares this column to a nil UUID,
because an absent discount curve is null and not the zero UUID.
4.9. interpolation_method
Interpolation method for discount factors between pillars
(LOG_LINEAR_DISCOUNT, CUBIC_SPLINE, or the two-segment
FLAT_FORWARD_THEN_LOG_LINEAR the FOMC-dated short-end task uses).
Fixed vocabulary intrinsic to this record, not a reference to another
entity.
std::string("LOG_LINEAR_DISCOUNT")
4.10. day_count_convention
References day_count_fraction_type.code (e.g. A360, A365) –
the day-count convention applied per pillar during bootstrap.
std::string("A365")
4.11. tenor_convention_code
References tenor_convention.code – the convention this curve's pillars resolve
their dates under (e.g. RATES_SPOT_FOMC for the meeting-dated short end, whose
tenors step through a schedule, and RATES_SPOT_FORWARD for a curve that steps by
anchor offsets).
The config that owns the pillars owns the convention. It used to be guessed from the
source series' qualifier, where a -FOMC suffix meant the schedule-step convention:
that put how a curve is built at the mercy of how one of its series happens to be
spelled, and it stopped working when the series' identity became the ORE key its
points project to. A config whose series carry no such suffix still has to name it.
std::string("RATES_SPOT_FORWARD")
4.12. split_tenor_code
References tenor.code – the pillar tenor at which this curve
transitions from a short-end interpolation segment to a continuous
long-end one. For a single-segment interpolation_method, this
equals the curve's own last pillar's end_tenor_code (a genuine
value, not a sentinel). Soft reference, undeclared at the DB level –
matching ir_curve_template_entry's own start_tenor_code=/
=end_tenor_code tenor references, validated through the
tenor_resolution machinery at the application layer.
std::string("1Y")
5. SQL
5.1. Flags
5.2. Checks
| expression |
|---|
| "curve_family_role" in ('FUNDING', 'PROJECTION') |
| "name" <> '' |
| "index_tenor" <> '' |
| "day_count_convention" <> '' |
| "split_tenor_code" <> '' |
| "discount_curve_config_id" is null or "discount_curve_config_id" <> "id" |
| ("curve_family_role" = 'FUNDING' and "discount_curve_config_id" is null) or ("curve_family_role" = 'PROJECTION' and "discount_curve_config_id" is not null) |
5.3. Indexes
| name | columns | unique | current_only | where_extra |
|---|---|---|---|---|
| party_name | tenant_id, party_id, name | true | true |
6. Insert trigger
6.1. Validations
| column | validation_function |
|---|---|
| day_count_convention | ores_refdata_validate_day_count_fraction_type_fn |
6.1.1. discount_curve_config_id strict two-tier + no-chain guard
Enforces what the shape-level check constraint above cannot: that a
PROJECTION config's discount_curve_config_id actually points at a
config whose own curve_family_role is FUNDING (strict two-tier,
no chaining), not merely at some other row. Also guards the reverse
direction: because every write to this bitemporal table – including
an edit to an existing row – runs through this same insert trigger
(a new version is a fresh INSERT closing the prior row, never an
UPDATE), an edit that changes an existing FUNDING config's own
curve_family_role away from FUNDING while one or more active
PROJECTION configs still reference it is rejected here too –
otherwise those referencing rows would silently end up pointing at a
config that is no longer FUNDING, with nothing left to catch it.
if NEW.curve_family_role = 'PROJECTION' then if not exists ( select 1 from "ores_refdata_ir_curve_bootstrap_configs_tbl" where tenant_id = NEW.tenant_id and id = NEW.discount_curve_config_id and curve_family_role = 'FUNDING' and valid_to = ores_utility_infinity_timestamp_fn() ) then raise exception 'Invalid discount_curve_config_id: %. Must reference an active FUNDING config -- chaining a PROJECTION config off another PROJECTION config is not supported.', NEW.discount_curve_config_id using errcode = '23503'; end if; end if; if NEW.curve_family_role <> 'FUNDING' then if exists ( select 1 from "ores_refdata_ir_curve_bootstrap_configs_tbl" where tenant_id = NEW.tenant_id and discount_curve_config_id = NEW.id and curve_family_role = 'PROJECTION' and valid_to = ores_utility_infinity_timestamp_fn() ) then raise exception 'Cannot change curve_family_role away from FUNDING for %: still referenced as the discount curve by one or more active PROJECTION configs.', NEW.id using errcode = '23503'; end if; end if;
7. C++
7.1. Flags
7.2. Repository
7.3. Domain includes
#include <boost/uuid/uuid.hpp> #include <optional> #include <string>
7.4. Entity includes
#include <string>
7.5. Conventions
7.6. Custom repository methods
7.7. Presentation
The generated detail dialog and controller are both disabled outright
(.enabled: false above) rather than generated-then-discarded/hand-
edited: this entity's real editing surface is the hand-written
CurveBuilderWorkbench (see the Curve builder UI task for why – a
persistent, non-modal, three-tab workbench with Save/Bootstrap/Publish
actions rather than a save-and-close form), wired up by an equally
hand-written IrCurveBootstrapConfigController. Both lived in the
removed desktop client as ordinary hand-written files, outside
codegen's ownership, so regenerating this model never touched them –
the ores.refdata.calendar_date precedent, whose generated Controller
simply omitted Add/Edit-via-DetailDialog code when detail_dialog was
disabled, applied the same split. The generated list window, history
dialog, and client model were never this entity's real editing
surface. The
* Detail fields table below exists only so codegen has valid input
to generate the (unused) domain/protocol layer against – deliberately
minimal (plain line_edit throughout), not a real editing UX.
7.7.1. Detail fields
| field | label | widget | type | is_key | is_required | placeholder |
|---|---|---|---|---|---|---|
| id | Id | idEdit | line_edit | true | true | |
| name | Name | nameEdit | line_edit | true | ||
| output_series_id | Output Series Id | outputSeriesIdEdit | line_edit | true | ||
| curve_family_role | Curve Family Role | curveFamilyRoleEdit | line_edit | true | ||
| discount_curve_config_id | Discount Curve Config Id | discountCurveConfigIdEdit | line_edit | |||
| interpolation_method | Interpolation Method | interpolationMethodEdit | line_edit | true | ||
| day_count_convention | Day Count Convention | dayCountConventionEdit | line_edit | true | ||
| split_tenor_code | Split Tenor Code | splitTenorCodeEdit | line_edit | true |
7.7.2. Columns
| enum_name | field | header | type | width | is_badge | badge_key |
|---|---|---|---|---|---|---|
| Name | name | Name | string | auto | ||
| OutputSeriesId | output_series_id | Output Series | uuid | auto | ||
| CurveFamilyRole | curve_family_role | Role | string | 100 | ||
| DiscountCurveConfigId | discount_curve_config_id | Discount Curve Config | uuid | auto | ||
| InterpolationMethod | interpolation_method | Interpolation | string | 140 | ||
| DayCountConvention | day_count_convention | Day Count | string | 100 | ||
| SplitTenorCode | split_tenor_code | Split Tenor | string | 90 | ||
| Version | version | Version | int | 70 | ||
| ModifiedBy | modified_by | Modified By | string | auto | ||
| RecordedAt | recorded_at | Recorded At | timestamp | auto |
8. See also
- Curve Role – the (different) instrument-pricing role axis.
- ores.refdata – component group overview.