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:

  • name is 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_code and index_tenor are 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_code and curve_family_role are 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_method and day_count_convention are sortable.
  • discount_curve_config_id is 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

Emacs 29.3 (Org mode 9.6.15)