Task: Curve Template sub-config (ir_curve_generation_config)
This page documents a task in the IR Rates synthetic data generation story. It captures the goal, current status, acceptance, and any notes or results.
Goal
Add a new sub-config type, codegen-driven like
fx_spot_generation_config
(projects/ores.synthetic/api/include/ores.synthetic.api/domain/fx_spot_generation_config.hpp,
model at
projects/ores.synthetic/modeling/ores.synthetic.fx_spot_generation_config.org),
owned by market_data_generation_config (whose docstring already
anticipated this: "vol surface, interest-rate curves later"). One
config per currency+index (USD-SOFR, EUR-ESTR, …), holding the
short-rate process parameters and the Curve Template: which tenors to
publish at which role (short-end deposit, mid-curve FRA/future,
long-end swap).
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | IR Rates synthetic data generation |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-03 |
Acceptance
- New codegen entity model + generated SQL/domain/repository/protocol
layers for the sub-config, following the
fx_spot_generation_configpattern. - Config holds: currency, index/curve name, short-rate process parameters, and an ordered list of (tenor label, instrument role) pairs forming the Curve Template.
- Each (tenor, instrument role) entry's published rate is derived
from the short-rate process's
P(t,T)zero-coupon bond price (see the short-rate-process task's Acceptance) at that tenor's maturity — a simple rate for deposits, an implied forward rate between two maturities for FRAs, a par-rate solve across the template's swap cashflow dates for swaps — never an independently-noised value per tenor. This is what makes the tick batch a slice of one consistent latent curve rather than an incoherent grid. - Qt UI CRUD for the config is out of scope for the first pass unless
trivial to add alongside the existing
FxSpotRateEditor-adjacent scaffolding — flag as a follow-up if deferred.
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.)
Generalised "instrument role" into a full, reusable product catalogue rather than a narrow 3-value enum, since the same classification is useful beyond this one config:
asset_class_code(ores.refdata): book_status-pattern code table (fx, rates, credit, equity, commodity, inflation, bond, cross_asset).instrument_code(ores.refdata): full ORE product/trade-type catalogue, sourced from ORE's own authoritativeoreTradeTypeenumeration inexternal/ore/xsd/instruments.xsd(114 entries), each tagged with itsasset_class, plus one ORE Studio-specific addition,Deposit(ORE has no distinct money-market-deposit trade type of its own). Both entities got dynamic-combo Qt CRUD as a side effect of codegen, wired intoRefdataPluginunder a new "Classifications" submenu.ir_curve_generation_config(ores.synthetic): party+config-scoped sub-config, one per currency+index, holding the short-rate process parameters (process_type=/=kappa=/=theta=/=sigma=/=initial_rate) plusticks_per_hour=/=enabled.thetastored as a single constant (not a term-structure path) per an explicit scope decision.ir_curve_template_entry(ores.synthetic): ordered (sequence_index,tenor_code,instrument_code) rows owned by a config;instrument_codeis FK-validated against the new catalogue via the standard Insert-trigger Validations mechanism;tenor_codeis left unvalidated, matching the existingtenor_convention_resolutionprecedent (no FK enforcement exists for tenor codes anywhere else in the codebase either).curve_instrument_pricer(ores.analytics.quant): pure static functions derivingdeposit_rate=/=fra_rate=/=swap_par_ratefromdiscount_factor()values produced by the short-rate processes, with input validation and a Catch2 suite including an integration test against realhull_white_process=/ =vasicek_processinstances.ores.synthetic.servicehas nopostgres_event_source=/=event_businfrastructure at all (a pre-existing gap shared by sibling entitiesfx_spot_generation_config=/=gmm_component), so the codegen-produced NATS event-registrar files for the two new synthetic entities were deleted rather than wired — out of scope for this task to fix.- Qt CRUD for
ir_curve_generation_config=/=ir_curve_template_entrythemselves was generated by codegen as a side effect but not wired into a plugin menu, per the acceptance criteria marking it out-of-scope for this pass.
Notes
Verified end-to-end by hand via direct SQL against a freshly
recreated dev database (no provisioning data available, so config
rows themselves could not be inserted without a full tenant/party
setup): confirmed ores_refdata_validate_instrument_code_fn — the
same function ir_curve_template_entry's insert trigger calls —
accepts Deposit, ForwardRateAgreement and Swap, and correctly
rejects the wrong-cased DEPOSIT while listing Deposit among the
valid codes.
PRs
| PR | Title |
|---|---|
| #1564 | [refdata,synthetic,analytics.quant,qt] Curve Template sub-config, product catalogue, pricing derivations |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Missing RBAC permission catalogue entries for the four new entities | iam_permissions_populate.sql | Accepted | Added read/write/delete rows for refdata::asset_class_codes, refdata::instrument_codes, synthetic::ir_curve_generation_configs, synthetic::ir_curve_template_entries, mirroring the fx_spot_generation_config/gmm_component precedent. |
| 2 | clock_timestamp() vs current_timestamp inconsistency within ores_synthetic_* schema | synthetic_ir_curve_*_create.sql | Declined | Deliberate, matches the convention already used by every ores_refdata_* table; a conscious call-out, not an accidental inconsistency. Migrating the older synthetic tables is a separate, out-of-scope concern. |
| 3 | Non-alphabetical include/registration order (nit) | registrar.cpp, LookupFetcher.hpp, RefdataPlugin.cpp | Declined | Functionally harmless; repo's nightly-format workflow re-sorts includes automatically on main rather than gating PRs. |
| 4 | tenor_code not FK-validated, unlike instrument_code | synthetic_ir_curve_template_entries_create.sql | Declined | Matches the existing tenor_convention_resolution precedent — no FK enforcement exists for tenor codes anywhere else in the codebase either; a deliberate scope decision, not an oversight. |
| 5 | No domain validation on kappa/initial_rate per process_type | synthetic_ir_curve_generation_configs_create.sql | Partially accepted | Added a CIR-specific check (initial_rate >= 0, required by CIR's square-root diffusion). Left kappa unconstrained: kappa <= 0 is a valid, intentional degenerate case (hull_white_process's target-level drift form degenerates exactly to ou_process when kappa <= 0 with constant theta). |
| 6 | Integration test asserts isfinite() only, doesn't test the claimed monotonic-consistency property | curve_instrument_pricer_tests.cpp | Accepted | Added the actual bounds assertion (swap rate sits between the shortest and longest single-period forward rates). |
Result
Delivered the full Curve Template sub-config stack: a generalised
asset_class_code=/=instrument_code product catalogue (114 real ORE
oreTradeType entries + 1 documented synthetic Deposit addition,
115 total) in ores.refdata with dynamic-combo Qt CRUD;
ir_curve_generation_config + ordered ir_curve_template_entry rows
in ores.synthetic following the fx_spot_generation_config=/
=gmm_component pattern with FK-validated instrument codes; and a
curve_instrument_pricer in ores.analytics.quant deriving
deposit/FRA/swap par rates from a short-rate process's
discount_factor(). Local build (linux-clang-debug-make) is clean
and all 73/73 ctest suites pass, including the new
curve_instrument_pricer_tests suite. Qt CRUD for the two synthetic
entities and actual tick-batch publishing remain out of scope per the
task's own acceptance criteria (the latter is tracked as a separate
BACKLOG task).