Task: Tick-batch publishing and persistence for curve instruments
Table of Contents
- Goal
- Status
- Acceptance
- Plan
- Analysis: two-phase design (family feed vs. disaggregation) and market-data grouping (2026-07-16)
- Analysis: Market Simulator IR support – plan (2026-07-18)
- Analysis: complete rates process editor – plan (2026-07-18, continued)
- Analysis: aligning IrCurveEditor with FxSpotRateEditor (2026-07-19)
- Notes
- Test Scenarios
- PRs
- Review
- Result
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
Extend the FX spot streaming pipeline
(projects/ores.synthetic/service/src/feed_controller.hpp,
fx_spot_feed) to the "one process step → N tenor values" shape a
curve needs, unlike FX spot's one process step → one scalar. Each
generation step publishes a tick batch: N individual NATS ticks (one
per Curve Template tenor), each landing as its own
market_observation row sharing one observation_datetime
(asset_class::rates, series_subclass::yield/fra/basis per
instrument role, point_id = tenor label) — mirroring the source=/
=point_id fix already made to feed_ingest_loop.cpp for FX spot in
the GMM improvements story.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | IR Rates synthetic data generation |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-16 |
Acceptance
- One short-rate process step yields a batch of N tenor values (per
the Curve Template); each is published as its own tick, all sharing
one
observation_datetime. - Ingest persists each tick as its own
market_observationrow with correctasset_class=/=series_subclass=/=point_id— no schema changes required (already supports this). - Confirms the shared
observation_datetimeis sufficient signal for a future bootstrap consumer to group a batch into one generation cycle, without building that consumer.
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.)
Design discussion (before implementation)
A proper swap par rate needs its fixed-leg payment schedule (a strip of periodic discount factors), not just its two endpoints — treating a Swap entry as a single-period deposit-equivalent (an early draft of this plan) was rejected as defeating the purpose of modelling a swap at all. This requires:
- A genuine, catalog-driven classification of "how do I price this
instrument against a curve" (deposit / fra / swap), rather than
inferring it from
start_tenor_code ="SPOT"= (Deposit and Swap are both point instruments but need different formulas). - A payment frequency for the swap's fixed leg, to build its intermediate payment dates between spot and maturity.
Payment frequency: consolidating a genuine duplication
ORE's canonical frequencyType enumeration
(external/ore/xsd/ore_types.xsd: Once, Annual, Semiannual,
Quarterly, Bimonthly, Monthly, Lunarmonth, Weekly, Daily)
was already modelled once, as ores.trading.payment_frequency_type
(used by trade-leg conventions). Building a second, separate
ores.refdata entity for the same concept (an early draft of this
plan) was rejected once spotted as a real duplication, not a
justified split — there is no case for two types here ("payment
frequency conventions" vs "payment frequencies" is a flimsy
distinction); it is one type with properties.
Resolved: a single payment_frequency entity, promoted into
ores.refdata (genuinely shared reference data — used by trading leg
conventions and curve-template schedule building, not
trading-owned), replacing ores.trading.payment_frequency_type
entirely:
code(PK): the canonical long-form codes above.period_unit: FK-validated againsttenor_unit.code(DAY=/=WEEK=/=MONTH=/=YEAR=/=NONE) via the standard Insert-trigger Validations mechanism — reuses the existingtenor_unitcatalog (including itsNONEsentinel row) rather than duplicating the vocabulary a second time as an inlineCHECK.period_multiplier(nullable int): null only whenperiod_unit=NONE(i.e.Once).- Tenant scope: system. This is a statement of fact (ORE's own
enumeration), not a per-tenant convention — same category as
tenor_unit=/=book_status, notswap_convention. - Badge on
period_unit(the referencing column), reusingtenor_unit's already-seeded badge domain/mappings (colors forDAY=/=WEEK=/=MONTH=/=YEAR=/=NONEwere seeded earlier this sprint) — no new badge definitions needed. Not badged on its owncode: an earlier draft self-badgedcode, which was caught as breaking the established "badge the referencing column, not the code table itself" convention (instrument_code.asset_class, notasset_class_code.code). - Migration: delete
ores.trading.payment_frequency_type(entity + every generated layer + SQL + Qt UI + menu wiring), and repoint everyores.tradingcolumn that stores a payment-frequency code string to validate against the newores.refdata.payment_frequencyinstead —swap_leg.payment_frequency_code(the only one with a real FK today), pluscredit_instrument.payment_frequency_code,commodity_instrument.payment_frequency_code, andequity_swap_instrument.payment_frequency(currently unvalidated free text) — wiring FK validation onto all of them is a deliberate test of the consolidated data model, not just a like-for-like swap.
Still to design (not yet resolved)
instrument_code'scurve_roleclassification (deposit/fra/swap/none).ir_curve_generation_config.fixed_leg_payment_frequency(FK to the newpayment_frequency) and the periodic-schedule-building pure function it feeds.- The
ir_curve_feedproducer itself and ingest-side wiring.
Payment frequency migration: completed
The payment_frequency consolidation described above is implemented:
- New
ores.refdata.payment_frequencyentity (full codegen facet set: API/core/service/Qt/SQL), seeded with the 9 canonical rows, badge onperiod_unitreusingtenor_unit's existing badge domain, DQ publish-from-dq wired. ores.trading.payment_frequency_typedeleted entirely: modeling.org, all generated API/core C++, Qt list/detail/history dialogs + menu wiring, CLI entity registration, and itsores.trading.module.orgcatalog row.- All four
ores.tradingconsumer columns (swap_leg=/=credit_instrument=/=commodity_instrument=/=equity_swap_instrument) repointed to validate against the new entity, including the three that previously had no FK validation at all. - Schema validated clean (253 tables, 0 warnings) after
db recreate.
Bugs found and fixed while UI-testing the migration
Testing the payment-frequency UI and ORE trade re-import surfaced three genuine, pre-existing bugs unrelated to the migration design itself, exposed by finally adding real FK validation where none existed before:
- History provider not registered:
ores.refdata/core/src/messaging/registrar.cppnever wired ahistory_registry().register_history_provider(...)entry forores.refdata.payment_frequency(onlycurrencyandcountryhad one) — added it, mirroring the existing pattern. - ORE importer stored raw tenor strings, not frequency names: every
mapper that populates a payment-frequency FK column
(
swap_instrument_mapper.cpp,credit_instrument_mapper.cpp,equity_instrument_mapper.cpp) copied the ORE schedule rule'sTenorstring (e.g."3M") straight into the FK column instead of converting it to the canonical name (Quarterly) the new FK validates against. Added a shared, bidirectionaltenor_to_payment_frequency()=/=payment_frequency_to_tenor()conversion in a newores.ore.core/domain/payment_frequency_conversion.hpp, wired into both forward (import) and reverse (export) directions. - Missing
trade_type_codeon 8 swap-family instrument types:swap_instrument_mapper.cpp'sforward_swap=/=forward_inflation_swap=/ =forward_fra=/=forward_capfloor=/=forward_swaption=/=forward_callable_swap=/ =forward_flexi_swap=/=forward_balance_guaranteed_swapnever setidentity.trade_type_code, so every one of these instruments failed its NOT-NULL check on import. Set it explicitly per function. stamp()silently no-op'd on nested audit fields: the documentedores::service::messaging::stamp()helper (which is supposed to overwritemodified_by=/=performed_byfrom the request context) was never called by any of the 8 trading instrument-save handlers (bond=/=commodity=/=composite=/=credit=/=rates[9 variants]/=scripted=/typed_equity=/=typed_fx), so every ORE-imported instrument kept the mapper's hardcoded, invalid"ores"literal formodified_by. Fixed by callingstamp(req->data.audit, ctx)in each handler — the fields live under a nestedauditsub-struct (ores::dq::domain::audit_record), not directly on the instrument, which is whystamp(req->data, ctx)'sif constexpr (requires {...})check was silently matching nothing.
Confirmed via ores.trading.service logs before/after: "Invalid
modified_by: ores", "Invalid payment_frequency: 3M…", and "Invalid
trade_type: value cannot be null or empty" are all gone after the fix
and rebuild. Left out of scope (unrelated to payment_frequency, called
out explicitly rather than silently ignored): check-constraint
violations on composite=/=scripted=/=commodity=/=swaption=/=credit
instruments (trade_type_code, quantity, unit, average_type,
script_name, long_short, tenor fields with other unmapped/invalid
values) and invalid input syntax for type date: "" on
bond=/=equity_swap=/=vanilla_swap=/=cap_floor imports — pre-existing
ORE-importer gaps for instrument types the payment_frequency migration
doesn't touch.
Analysis: two-phase design (family feed vs. disaggregation) and market-data grouping (2026-07-16)
Phase split: family feed vs. per-point disaggregation
The producer side splits into two independent concerns, not one task:
- Phase 1 (this task's actual scope): the family feed. One
ir_curve_feed, mirroringfx_spot_feed, where one short-rate process step fans out to Ncurve_instrument_pricercalls (one perir_curve_template_entry) and publishes N ticks synchronously, sharing oneobservation_datetime– exactly this task's original Acceptance. - Phase 2 (not this task): per-point disaggregation/staggered
timing. Making each tenor in a family tick independently, at its
own arrival time, rather than all N points landing together. This
is not IR-specific: it is the same concern as the existing
BACKLOG story Stochastic tick arrival times for synthetic feeds,
whose
arrival_profile_configabstraction is designed to wrap any feed. Applying it perpoint_idinstead of persourcefor curve feeds is that story's future work, not a bespoke IR disaggregator – so Phase 1 does not need to anticipate any special interface for it. No task filed here for Phase 2; tracked via the link to that story instead.
Two subjects, not two message shapes
The family feed publishes on its own namespace,
synthetic.v1.curve_family.<source>, distinct from the plain-tick
namespace synthetic.v1.tick.<source> scalar feeds already use –
still N individual NATS messages (not one aggregate payload), so
ingest stays a dumb per-message consumer. When Phase 2 eventually
lands, the disaggregation adapter would subscribe to
curve_family.* and republish onto tick.*, so from ingest's point
of view a disaggregated IR point looks identical to any other tick
(already carries point_id) – no ingest-side special-casing needed
either now or later. A consumer wanting the raw family (e.g. a future
bootstrap-testing consumer) subscribes to curve_family.*; a
consumer simulating live market consumption subscribes to tick.*.
This is a producer/subject-routing decision, not a per-config
"family vs individual" toggle.
What the wire format is not: one aggregate JSON per curve
Considered and rejected: publishing one large JSON object containing
the whole curve per generation step, so a UI could "open it up and
plot the curves" directly. Rejected because market_observation is
already row-per-point (series_id, observation_datetime,
point_id) – the same shape non-scalar series (vol surfaces)
already use – and an aggregate wire object would either force ingest
to explode it back into rows anyway, or require a new non-relational
storage shape outside this story's scope. It would also break Phase
2 cleanly, which needs to peel off individual points to re-time them
independently – an aggregate object is strictly worse for that than
row-per-tick. "Open it up and plot it" is a read/query-side
concern instead – see the new curve-snapshot-builder-viewer task.
Analysis: does market data need a batch/generation identifier?
Raised: since a tick batch's points may one day be published at
staggered times (Phase 2), how does a consumer know which points
belong to the same coherent draw, if not by matching
observation_datetime? Considered adding a generation_id=/
=batch_id column to market_observation, stamped once per process
draw and carried by every point derived from it, so grouping survives
staggered publish times.
Rejected after checking how this actually works in the real world
(no vendor feed exists in this codebase to check against – ores.bloomberg
is an unstarted BACKLOG story – so checked the market data protocols
instead):
- FIX MarketDataSnapshotFullRefresh (W): always one instrument per
message, even when a single
MDReqIDgroups a request for many instruments – responses are still separate messages per instrument. No message-level construct ties multiple instruments' data together as one coherent unit. - Refinitiv/LSEG RIC records: each instrument (e.g.
USSW10Y=for a 10Y swap rate) is its own independent real-time record, ticking on its own schedule. A "curve" is not a wire-level object at all – it is many independently-updating RICs a downstream consumer groups by naming convention.
Conclusion: no batch/generation identifier exists in real market
data feeds, and none is added here either. Vendor feeds never assert
"these N points are mutually consistent, published together" –
a curve is reconstructed downstream, by taking the latest tick <= an
as-of time independently per point. Adding a synthetic-only
batch_id would have been exactly the kind of source-specific
extension the architecture (see Market Data Architecture) is meant to
avoid. The correct pattern is a read-side as-of query –
SELECT DISTINCT ON (point_id) ... WHERE series_id = ? AND
observation_datetime < ? ORDER BY point_id, observation_datetime
DESC= – which degenerates correctly to "all points share one
timestamp" in this task's Phase-1 synchronous-publish case, and keeps
working unmodified once Phase 2 staggers publish times. No schema
change. Tracked as its own task: curve-snapshot-builder-viewer.
Analysis: Market Simulator IR support – plan (2026-07-18)
Live UI testing found the first Market Simulator integration attempt built a parallel, IR-specific structure instead of reusing the existing FX machinery generically, per explicit correction ("we do not want a separate control for IR … use existing logic in simulator"). Three concrete defects identified by reading the code (not guessed):
- Parallel folder tree:
ir_curve_generation_confighas nofolder_idcolumn, unlikefx_spot_generation_config. Meanwhilesynthetic_publish_from_dq_create.sqlalready resolves/creates the real folder chain (Root -> Collection -> "Rates" -> "IR Curves") for IR curves and then has nowhere to store it.buildTree()worked around the missing column by inventing a synthetic flat "IR Curves" group directly under root – a second tree bypassing the real folders already in the DB. - Two flags for one currency:
buildIrCurveFeedItem()called the FX pair-flag overloadcurrency_flag_icon(imageCache, ccy, ccy)(base+quote composite) with the same code twice, instead of the existing single-flag overloadcurrency_flag_icon(imageCache, isoCode)already used elsewhere (ClientBookModel,ClientCrmTopologyConfigModel) for single-currency entities. - No summary/chart for IR leaves:
showSummaryForCurrent()'sNodeType::Feedcase only ever looks upfxPairs_and callsshowFxPairSummary(); it never checksirCurves_, so selecting an IR curve leaf shows nothing – no hero, no tick chart, no start/stop detail panel.
Plan
- Add
folder_idtoir_curve_generation_config(org spec + regenerate: SQL table, C++ domain struct, repository, JSON I/O), mirroringfx_spot_generation_config. - Update
synthetic_publish_from_dq_create.sql's IR insert to store thev_folder_idit already computes but currently discards. - Rewrite
buildTree()to fold IR curves into the same generic folder walk as FX (now thatfolder_idexists) instead of the separate flat-group special case – one tree-building path for both asset classes, extensible to future asset classes without a new parallel branch each time. - Fix the flag icon in
buildIrCurveFeedItem()to the single-flag overload. - Add IR curve summary/hero support: extend
showSummaryForCurrent()'s Feed case to checkirCurves_too, addshowIrCurveSummary()(single-flag hero, reusing the existing tick chart subscription machinery keyed byirCurveSourceName()). - Build, restart services/client with
--open-scenario, re-verify live in Market Simulator.
Steps 1-6: done
Confirmed live: folder_id regenerated across all codegen layers
(SQL/domain/repository/JSON I/O), synthetic_publish_from_dq_create.sql
now stores the v_folder_id it resolves, and after a DB recreate +
Barclays re-provision the IR curve configs' folder_id correctly
points at the real Root -> Realistic -> Rates -> "IR Curves" folder
(ba1e1e67-...) – queried directly, not guessed. buildTree() now
folds IR curves into the same folder walk as FX
(irCurvesByFolder=/=pairsByFolder are structurally identical). Flag
icon fixed to the single-currency overload. Naming uses idiomatic
rates convention (space-separated, e.g. "USD SOFR"), not FX's
slash-separated pair notation.
Analysis: complete rates process editor – plan (2026-07-18, continued)
Testing steps 1-6 surfaced a further gap: Market Simulator's Edit
action for an IR curve leaf does nothing (editEntity()'s Feed case
only ever looks up fxPairs_, silently no-ops for IR curves) –
there is currently no way to see or change a curve's process
parameters (kappa/theta/sigma/etc.) or its Curve Template (tenor
grid) from the Simulator at all. Read FxSpotRateEditor (2200+
lines) to understand the FX shape before designing the rates
equivalent, rather than guessing an interface.
Mapping FX concepts to rates
FX (FxSpotRateEditor) |
Rates equivalent | Note |
|---|---|---|
gmm_component mixture (N components) |
no equivalent – one process (Vasicek/CIR/Hull-White) | Simple/Advanced mixture-table split doesn't apply; one param set only. |
| price-path chart (closed-form GBM/OU, client-side) | short-rate sample-path chart (closed-form Euler-Maruyama, client-side) | Vasicek is an OU process – FX's existing "ou" slider/half-life machinery is directly reusable, not reinvented. |
| (nothing) | curve-shape preview chart (rate vs. tenor) | Needs IYieldCurveProcess::discount_factor() + curve_instrument_pricer, which live server-side only (ores.analytics.quant=/=ores.synthetic.service) – a new non-persisting preview NATS request, not a client-side reimplementation. |
gmm_component table (Advanced tab) |
ir_curve_template_entry table (tenor grid) |
Same one-config-many-children shape; entity already has full CRUD codegen (repository/protocol/mapper) – this is new editor UI wiring an existing entity, not new backend modeling. |
| Provenance tab | Provenance tab | Shared DetailDialogBase widget, no change. |
Confirmed reusable without duplication (checked, not assumed):
ores.synthetic.api/domain/curve_template_validation.hpp/.cpp(validate_curve_template) is a pure function in theapilayer (linked into Qt already via domain structs) – the tenor-overlap check can run client-side in the Curve Template tab exactly as written, no server round-trip needed for that part.yield_curve_process_type,ores.refdata.instrument_code,ores.refdata.payment_frequency,ores.refdata.tenorall have full protocol/repository codegen already – every combo box in the wireframe below is backed by an existing request, none new.curve_instrument_pricer(ores.analytics.quant) andIYieldCurveProcess::discount_factor()are service-layer only – confirmed Qt has no dependency onores.analytics.quantorores.synthetic.service, so the curve-shape chart genuinely needs a new preview request, not a missed existing one.
Wireframe
┌─ IR Curve: USD SOFR ──────────────────────────────────────── [Save] [Cancel] ┐ │ [Instrument] [Process] [Curve Template] [Provenance] │ ├────────────────────────────────────────────────────────────────────────────┤ │ INSTRUMENT TAB │ │ Currency: [USD ▼] Index Name: [SOFR________] │ │ Fixed leg freq: [Annual ▼] Enabled: [x] │ │ New tick every: [1____] sec (from ticks_per_hour) │ │ Source: ir_curve.usd.sofr (read-only, derived) │ └────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────────────┤ │ PROCESS TAB │ │ Engine: (•) Vasicek ( ) CIR ( ) Hull-White │ │ │ │ Initial rate r0 [====|=========] 3.25 % │ │ Mean level θ [======|=======] 3.50 % │ │ Reversion κ [===|==========] half-life ≈ 45 days (log-scale slider) │ │ Volatility σ [==|===========] 0.80 % │ │ │ │ ┌─ Sample short-rate paths (5 draws) ──────┐ ┌─ Curve shape preview ──────┐ │ │ │ ╱‾╲___ │ │ rate │ │ │ │ ___╱ ╲___╱‾╲___ │ │ │ ╱‾‾‾‾ │ │ │ │ ╱ ╲___ │ │ │ ╱‾╱ │ │ │ │______________________________ ticks │ │ │__╱____________ tenor │ │ │ └───────────────────────────────────────────┘ └─────────────────────────────┘│ └────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────────────┤ │ CURVE TEMPLATE TAB │ │ # │ Start Tenor │ End Tenor │ Instrument │ │ │ 0 │ SPOT │ 1M │ Deposit ▼ │ [↑][↓][Remove] │ │ 1 │ SPOT │ 3M │ Deposit ▼ │ [↑][↓][Remove] │ │ 2 │ 3M │ 6M │ ForwardRateAgree ▼ │ [↑][↓][Remove] │ │ 3 │ SPOT │ 2Y │ Swap ▼ │ [↑][↓][Remove] │ │ [+ Add tenor entry] │ │ ⚠ warning banner here if tenor windows overlap (validate_curve_template) │ └────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────────────┤ │ PROVENANCE TAB — shared DetailDialogBase widget, same as every other entity │ └────────────────────────────────────────────────────────────────────────────┘
Plan
Backend: non-persisting curve preview request
- Design
preview_ir_curve_request=/=responseprotocol inores.synthetic.api/messaging/ir_curve_preview_protocol.hpp: request carries process_type/kappa/theta/sigma/initial_rate plus the current (possibly unsaved) Curve Template entry list; response carries, per entry, {sequence_index, start_tenor_code, end_tenor_code, tenor label, rate}. Implement the handler in
ores.synthetic.servicereusingbuild_ir_curve_refdata_context()+ir_curve_template_resolver::resolve()curve_instrument_pricerexactly asir_curve_feeddoes today –
compute only, no DB writes.
- Wire the NATS subject + registrar entry (mirrors every other
synthetic handler's request-scoped
make_request_context()pattern). - No SQL/schema change – pure compute endpoint.
Qt: charts
- Short-rate sample-path chart: new lightweight chart widget doing
closed-form Euler-Maruyama simulation (
dr = κ(θ−r)dt + σ dW, CIR adds√r) client-side – same category of local mathSamplePricePathsChartalready does for FX, reusing that component's shape if generic enough or a sibling class if not. - Curve-shape preview chart: new chart widget, populated by a debounced call to the new preview request on every slider/table change.
Qt: IrCurveEditor widget
- New
IrCurveEditor.hpp/.cppextendingDetailDialogBase, two constructors (new/edit) mirroringFxSpotRateEditor's shape. - Instrument tab: currency combo (reuse
populateCurrencyCombo-style pattern), index name line edit, fixed leg payment frequency combo (ores.refdata.payment_frequency), enabled checkbox, ticks-per-hour spin + human-readable echo, read-only derived source-name label. - Process tab: engine combo sourced from
yield_curve_process_type; κ/θ/σ/r0 sliders + spinboxes reusing FX's "ou" log-scale-κ/half-life pattern, per-engine label/tooltip variants (Vasicek/CIR/Hull-White); wires both new charts. - Curve Template tab:
QTableWidgetofir_curve_template_entryrows – tenor combos sourced fromores.refdata.tenor, instrument combo sourced fromores.refdata.instrument_code(filtered by curve role), add/remove/reorder buttons, client-sidevalidate_curve_templateoverlap check with a warning banner. - Provenance tab: reuse the shared widget, no change.
- Save: persist config fields via the existing
ir_curve_generation_config_protocol, diff-sync template entries (insert/update/delete againstoriginalEntryIds_) via the existingir_curve_template_entry_protocol– same patternFxSpotRateEditoruses forgmm_componentrows.
MarketSimulatorWindow wiring
- Replace the no-op IR branch in
editEntity()withopenIrCurveEditorForEdit(), opening the newIrCurveEditorin aDetachableMdiSubWindow, mirroringopenFxEditorForEdit(). - Add the equivalent "New IR Curve" creation path, mirroring
onNewFxRateClicked()=/=openFxEditorForNew().
Verification
- Build (
ores.qt.synthetic.libfirst, then full build). - No DB recreate needed (no schema change in this phase).
- Restart services/client with
--open-scenario, live-verify: create a new IR curve process end-to-end (Instrument -> Process with live charts -> Curve Template -> Save), edit an existing one (DQ-published Barclays USD SOFR/EUR ESTR/GBP SONIA), confirm ticks reflect edited parameters.
Analysis: aligning IrCurveEditor with FxSpotRateEditor (2026-07-19)
Live review of the shipped editor found real, verifiable structural
gaps against FxSpotRateEditor – not vague "doesn't look right"
feedback, but confirmed by reading both dialogs' source side by side.
Per-item decision below: copy FX where there is no IR-specific reason
to differ (a spurious difference), diverge only where rates genuinely
warrant it (stated explicitly), never a blind copy either way.
Confirmed differences and decisions
- Simple/Advanced toggle: FX uses two checkable/auto-exclusive
QPushButton's, explicitly QSS-styled into a joined accent-colour pill (min-height: 30px, bold, filled when checked), placed at the right end of the header row. The shipped editor used two plainQRadioButton's with no styling, which is why it read as "no toggle button at all". Decision: copy FX verbatim – no IR reason for this affordance to look different. - Simple-mode sliders: FX has no spinbox beside the slider – a title + gray value-echo label sits above a full-width slider; precise entry is Advanced-only. The shipped editor paired every slider with an adjacent spinbox in Simple mode. Decision: copy FX – drop the paired spinbox, echo the value in a label instead; Advanced's table remains the only precise-entry surface.
- Three-zone tab layout: FX is header row (engine + frequency +
toggle) / middle row (controls left + compact chart right, capped
~380px) / bottom row (prominent full-width chart in its own titled
QGroupBox). The shipped editor stacked both charts full-width below the controls – no side-by-side zone at all. Decision: copy FX's structure, but invert which chart is compact vs prominent – FX's compact side chart is the supporting stat (return distribution), prominent bottom chart is the hero view (sample paths). For IR the priority is reversed: curve shape is the "what will I actually publish" hero view (curve shape is a genuine IR-only chart FX has no equivalent of), sample paths is the supporting/exploratory view. So: middle row = controls + compact sample-paths; bottom row = prominent full-width curve-shape. Same structural pattern, content assignment flipped for a stated reason. - Currency combo: FX's
baseCombo_=/=quoteCombo_are editable with aQCompleter(PopupCompletion,MatchContains, case-insensitive) over the typedOreCurrencyComboBoxclass. The shipped editor used a plain non-editableQComboBox. Decision: copy FX – same ~180-row ISO currency list, same "pick 1 of many quickly" use case, no IR reason to make this worse. - Index name combo: Decision: deliberately not like FX's currency combos – post-filter (currency-scoped, two-segment codes only) this list is typically 1-3 items, not ~180; a completer would solve a problem that doesn't exist here. Stays a plain non-editable combo.
- Fixed-leg-frequency / engine combos: short fixed enumerations (9
and 3 rows) – matches the codegen "standard" (Portfolio's
purposeTypeCombo=/=statusCombo, also plain non-editable, no completer anywhere in the codebase for combos this short). No change – already correct. - Instrument tab framing: every codegen
.uidialog (Portfolio, Book) wraps itsQFormLayoutfields in a titledQGroupBox(e.g. "Basic Information"); the shipped editor put the form directly on the tab with no framing. Decision: add one ("Curve Identity"). - insertPolicy: every codegen
.uicombo explicitly declaresQComboBox::NoInsert; the shipped editor never sets it (functionally inert since none are editable except the currency combo post-item-4, but cheap to match literally). Decision: set it explicitly. - Curve Template table combos: already the only precedent in the codebase for inline table-cell combos (FX's component table); already matches it. No change.
Notes
Test Scenarios
| Scenario | State | Notes |
|---|---|---|
| Verify ir_curve_feed tick-batch publishing | PENDING | Manual verification via Qt client. |
PRs
Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
Result
Shipped well beyond the original Phase-1 Acceptance (ir_curve_feed
tick-batch publishing), which itself was completed and independently
verified via direct SQL before this session's work began. This
session took the feature from "backend mechanism works" to "usable
end-to-end through the real UI":
ir_curve_generation_configgainedfolder_id(real folder-tree placement, matchingfx_spot_generation_config) and a persisted, editablesource_namecolumn following FX's exactsynthetic.<collection>.<pair>convention (replacing a computedir_curve.<ccy>.<idx>string every consumer had to re-derive).index_nameis now FK-validated againstores.refdata.floating_index_type(stored as the full ISDA code to reuse that entity's existing validator) instead of free text.MarketSimulatorWindow("Market Simulator") now shows IR curves in the same real folder tree FX pairs use (no more parallel synthetic group), with correct single-flag icons and idiomatic rates naming ("USD SOFR").- A complete
IrCurveEditor(Instrument / Process / Curve Template / Provenance tabs) was built, aligned item-by-item againstFxSpotRateEditor(segmented Simple/Advanced toggle, slider shape, currency combo, chart zone layout) – copying FX where there was no IR-specific reason to differ, diverging deliberately (curve-shape chart as the hero view, not sample-paths) where rates genuinely warranted it. Two new stateless preview NATS endpoints (synthetic.v1.ir_curve.simulate_paths,synthetic.v1.ir_curve.preview_shape) back its live charts. - Market Simulator's own overview tick chart was fixed to route to the correct NATS subject/payload shape for IR curves (it silently never ticked before) and redesigned from an undifferentiated single line into a proper curve-shape overlay (last N batches, oldest faded, configurable via a spin box).
- The
synthetic.ir_curve_configs.realisticDQ dataset now seeds one overnight-RFR Vasicek curve per each of the top 20 currencies by FX turnover (was 3), each with its own realistic per-market calibration, backed by 15 newfloating_index_typereference rows. - Fixed several genuine, pre-existing bugs surfaced by being the first
real exerciser of these code paths: ccache worktree corruption,
JetStream stream-creation-vs-update semantics, a NATS subject
wildcard token-count mismatch, and (this session) Barclays never
having its own
floating_index_typerows (missing from the tenant provisioner's copy-from-system-tenant step) and an editable QComboBox's flag icon not following programmatic text changes.
Verified via the verify-ir-curve-feed test scenario: all 5 steps now
PASS through the real UI (previously 2 of 5 failed only because
Market Simulator had no IR view at all).
Deliberately deferred, each with its own filed follow-on task:
seed-ir-curve-sample-data (vintage sourcing, numerical-stability
tuning), normalize-floating-index-refdata (the
floating_index_type=/=overnight_index_convention duplication), and
dual-curve-discounting-projection-model (IBOR-style term indices
need a genuine discount/projection split; the currently-seeded
overnight RFRs are correctly single-curve as-is).