Task: IR curve bootstrap config: refdata entities for the bootstrap recipe
Table of Contents
This page documents a task in the IR curve bootstrapping + official curve republish story. It captures the goal, current status, acceptance, and any notes or results.
Goal
Record 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,
overnight_index_convention; not ores.marketdata, which owns only
the generic value store).
ores_refdata_ir_curve_bootstrap_configs_tbl:source_series_id(the rawRATES/YIELDmarket_seriesthis bootstraps),output_series_id(not null — minted at config-creation time, never deferred, so there is no "not yet published" null state to sentinel around),curve_family_role('FUNDING'|'PROJECTION'— a new axis, distinct from the existing instrument-pricingcurve_role),discount_curve_config_id(self-referencing;ores_utility_nil_uuid_fn()sentinel whencurve_family_role = 'FUNDING', a required FK when'PROJECTION'— this is what encodes Multi-Curve Construction's Funding-before-Projection build-order dependency as data. Strict two-tier only: the referenced config must itself havecurve_family_role = 'FUNDING', enforced by check constraint/trigger — Multi-Curve Construction is explicit that a Projection Curve depends on the Funding Curve only, never on another Projection Curve, and names basis-linked/cyclic Projection dependencies as an out-of-scope modelling gap, not something this design should silently permit by leaving the FK's target unconstrained; also guarded against self-reference,discount_curve_config_id <> id),interpolation_method,day_count_convention,split_tenor_code(a genuine tenor value, not a sentinel — for a single-segment method it equals the curve's own last pillar'send_tenor_code, matching the spirit ofir_curve_template_entries's existing'SPOT'tenor, which its own doc comment is explicit isn't a sentinel hack).ores_refdata_ir_curve_bootstrap_pillars_tbl:bootstrap_config_id,sequence_index,start_tenor_code=/=end_tenor_code(FK toores_refdata_tenors_tbl.code),curve_role_code(FK toores_refdata_curve_roles_tbl.code— DEPOSIT/FRA/SWAP). Same one-parent-many-children shape asir_curve_template_entries, including its unique-active-row index on(tenant_id, party_id, bootstrap_config_id, sequence_index)— but source-independent: it names a raw series + pillar list to bootstrap from, not a synthetic generation recipe.
Depends on the shared derivation-lineage task for the
ores_refdata_derivation_kinds_tbl values this config's output series
will be stamped with.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | IR curve bootstrapping + official curve republish |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Pick up ir-curve-bootstrapping-engine, the next task in this story. |
| Last touched | 2026-08-05 |
Acceptance
- Both tables exist in
ores.refdata, tenant/party-scoped, versioned, following the standard temporal-entity convention (valid_from=/ =valid_to, GIST exclusion, soft-update/delete triggers) already used byir_curve_template_entriesandcrm_topology_configs. - Creating a bootstrap config mints its
output_series_idmarket_seriesrow atomically — no code path can observe a config with a null/absent output series. discount_curve_config_idis enforced (check constraint) to be nil-uuid iffcurve_family_role = 'FUNDING', and a valid FK to another config's id iff'PROJECTION'— and, when set, that referenced config's owncurve_family_rolemust be'FUNDING'(strict two-tier, no chaining) and must not equal the referencing config's own id (no self-reference). Proven by a test attempting a PROJECTION-to-PROJECTION chain and a self-referencing row, both rejected.- Pillar rows validate
start_tenor_code=/=end_tenor_codeagainstores_refdata_tenors_tblandcurve_role_codeagainstores_refdata_curve_roles_tbl, exactly asir_curve_template_entriesalready does for its own tenor/instrument_code references, and carry the same(tenant_id, party_id, bootstrap_config_id, sequence_index)unique-active-row indexir_curve_template_entrieshas. - No new tenor or curve-role concepts invented — both are read-only references into the existing reference data.
Plan
(Implementation strategy. Written when work starts; key decisions
are distilled into the parent story's * Decisions at close, but the
plan itself stays — it is the historical record of what we did.)
Notes
Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario, run
through the QA Validation Runner panel) that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
PRs
| PR | Title |
|---|---|
| #1847 | [refdata,sql] IR curve bootstrap config recipe entities |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Garbled doc comment – verbatim span with an embedded literal =, same markup bug hit twice already in the shared-derivation-lineage task | ir_curve_bootstrap_config.org | Accepted | Reworded to avoid embedding = inside a verbatim span |
| 2 | Acceptance criteria claim pillar tenor validation is DB-enforced; implementation is a soft, application-layer reference matching ir_curve_template_entry's own precedent | task_ir-curve-bootstrap-config.org | Accepted | Added to Follow-ons, flagging the acceptance-bullet/implementation gap explicitly |
| 3 | Temporal consistency gap: nothing stopped an existing FUNDING config from being edited into PROJECTION (or any other role) while still referenced by active PROJECTION configs | ir_curve_bootstrap_config.org | Accepted | Added a second guard to the same insert-trigger block, since every edit to this bitemporal table runs through it as a fresh INSERT |
| 4 | output_series_id uniqueness scoped by (tenant_id, party_id), not tenant-wide – two different parties could each claim the same output series | ir_curve_bootstrap_config.org | Accepted | Dropped party_id from the natural key entirely; output_series_id is now the sole, tenant-wide natural key |
| 5 | Long generated index identifier (79 chars) exceeds PostgreSQL's 63-byte NAMEDATALEN limit on the pillar table | refdata_ir_curve_bootstrap_pillars_create.sql | Declined | Pre-existing, shared codegen-template naming gap (confirmed the same issue on ir_curve_template_entries and other siblings) – not a regression this task introduces, tracked as a follow-up against the shared template, not fixed one-off here |
| 6 | No dedicated automated test for the two-tier rejection paths | (none, hand-written trigger logic) | Declined | Matches current project convention for other recent codegen entities (curve_role, crm_topology_config, tenor – none have per-entity trigger tests either); verified instead via generated-SQL review and a clean db recreate, already disclosed in the PR description |
Result
Shipped: two new ores.refdata entities (schema + generated C++
domain/repository/service/messaging layer, no Qt UI – out of scope
for this task).
Schema
ir_curve_bootstrap_config: source/output series (soft cross-component references intoores.marketdata'smarket_series),curve_family_role(FUNDING=/=PROJECTION), self-referencingdiscount_curve_config_id,interpolation_method,day_count_convention(validated against the existingday_count_fraction_typetable),split_tenor_code(soft tenor reference, matchingir_curve_template_entry's own tenor columns).ir_curve_bootstrap_pillar: one-parent-many-children pillar list, FK to the parent config,curve_role_codevalidated against the existingcurve_roletable.output_series_idis the sole natural key, tenant-wide (not scoped byparty_id): exactly one active config, across the whole tenant, may produce a given output series. Went through two review-round fixes to get here: the first codegen pass produced a wrong one-config-per-party unique index; the round-1 review fix (party_id+output_series_id) still left two different parties able to each hold an active config for the same output series, fixed properly in the PR's review round by droppingparty_idfrom the natural key entirely.- Two-tier
discount_curve_config_idenforcement, both directions: a check constraint shape-validates (nil-uuid iffFUNDING; non-nil and not self-referencing iffPROJECTION); a custom insert-trigger block validates the referenced config is itselfFUNDING(no chaining) when inserting/editing aPROJECTIONconfig; and a second guard in the same trigger block rejects the reverse case – editing an existingFUNDINGconfig's owncurve_family_roleaway fromFUNDINGwhile one or more activePROJECTIONconfigs still reference it (every edit to this bitemporal table runs through the same insert trigger, so this closes a temporal-consistency gap the review round caught: nothing originally stopped a referencedFUNDINGconfig from later being edited into something else). Uses the same per-entity:implements <uuid>paste-marker mechanismcurve_role'sExtra dropsalready uses. - Dual RLS (tenant + party isolation) on both tables, matching
ir_curve_template_entry's own precedent for a party-scoped table.
Verified
- Local build clean (
linux-clang-debug-make). compass db recreate+ SQL schema validator clean (0 warnings) – also closed out theobservation_lineagesRLS_002gap left over from the previous task, and filed a capture for two pre-existing, unrelated, by-design warnings onsynthetic_market_data_generation_configs_tblrather than papering over them silently.refdata-codegen-driftcheck (check_component_drift.py) clean – regenerated output matches what's committed exactly.ctest -R "refdata": 4/4 test binaries passed.
Follow-ons not covered by this task
- Acceptance called for "creating a bootstrap config mints its
output_series_idrow atomically." This task only makesoutput_series_ida required column – the actual cross-component orchestration (a service call intoores.marketdatato create themarket_seriesrow before/alongside the config insert) is not implemented. That orchestration is inherently part of a later task (ir-curve-bootstrapping-engineorofficial-curve-republish, whichever ends up owning config creation), not schema work. Flagging explicitly rather than letting the acceptance bullet imply it's done. - Acceptance also called for tests proving a PROJECTION-to-PROJECTION
chain and a self-referencing row are both rejected. The trigger
logic was verified by reading the generated SQL and by a clean
compass db recreate, but no automated test (SQL or C++) exercises the rejection paths yet. Worth adding when the bootstrapping engine task starts writing real bootstrap configs and needs fixtures anyway. - Acceptance also stated pillar rows "validate
start_tenor_code=/ =end_tenor_codeagainstores_refdata_tenors_tbl." As implemented, those two columns are soft (undeclared at the DB level) references, validated only through thetenor_resolutionmachinery at the application layer – exactly matchingir_curve_template_entries's own precedent (confirmed: that sibling table's insert trigger doesn't DB-validate its tenor columns either), so this isn't a design flaw, but the acceptance bullet overstated what's actually enforced at the DB layer. Flagging the gap between the bullet's wording and the implementation rather than leaving it unstated.