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 raw RATES/YIELD market_series this 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-pricing curve_role), discount_curve_config_id (self-referencing; ores_utility_nil_uuid_fn() sentinel when curve_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 have curve_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's end_tenor_code, matching the spirit of ir_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 to ores_refdata_tenors_tbl.code), curve_role_code (FK to ores_refdata_curve_roles_tbl.code — DEPOSIT/FRA/SWAP). Same one-parent-many-children shape as ir_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 by ir_curve_template_entries and crm_topology_configs.
  • Creating a bootstrap config mints its output_series_id market_series row atomically — no code path can observe a config with a null/absent output series.
  • discount_curve_config_id is enforced (check constraint) to be nil-uuid iff curve_family_role = 'FUNDING', and a valid FK to another config's id iff 'PROJECTION' — and, when set, that referenced config's own curve_family_role must 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_code against ores_refdata_tenors_tbl and curve_role_code against ores_refdata_curve_roles_tbl, exactly as ir_curve_template_entries already does for its own tenor/instrument_code references, and carry the same (tenant_id, party_id, bootstrap_config_id, sequence_index) unique-active-row index ir_curve_template_entries has.
  • 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 into ores.marketdata's market_series), curve_family_role (FUNDING=/=PROJECTION), self-referencing discount_curve_config_id, interpolation_method, day_count_convention (validated against the existing day_count_fraction_type table), split_tenor_code (soft tenor reference, matching ir_curve_template_entry's own tenor columns).
  • ir_curve_bootstrap_pillar: one-parent-many-children pillar list, FK to the parent config, curve_role_code validated against the existing curve_role table.
  • output_series_id is the sole natural key, tenant-wide (not scoped by party_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 dropping party_id from the natural key entirely.
  • Two-tier discount_curve_config_id enforcement, both directions: a check constraint shape-validates (nil-uuid iff FUNDING; non-nil and not self-referencing iff PROJECTION); a custom insert-trigger block validates the referenced config is itself FUNDING (no chaining) when inserting/editing a PROJECTION config; and a second guard in the same trigger block rejects the reverse case – editing an existing FUNDING config's own curve_family_role away from FUNDING while one or more active PROJECTION configs 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 referenced FUNDING config from later being edited into something else). Uses the same per-entity :implements <uuid> paste-marker mechanism curve_role's Extra drops already 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 the observation_lineages RLS_002 gap left over from the previous task, and filed a capture for two pre-existing, unrelated, by-design warnings on synthetic_market_data_generation_configs_tbl rather than papering over them silently.
  • refdata-codegen-drift check (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_id row atomically." This task only makes output_series_id a required column – the actual cross-component orchestration (a service call into ores.marketdata to create the market_series row before/alongside the config insert) is not implemented. That orchestration is inherently part of a later task (ir-curve-bootstrapping-engine or official-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_code against ores_refdata_tenors_tbl." As implemented, those two columns are soft (undeclared at the DB level) references, validated only through the tenor_resolution machinery at the application layer – exactly matching ir_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.

Emacs 29.3 (Org mode 9.6.15)