Story: FOMC-dated OIS short end: calendar-event model and flat-forward pillars
Table of Contents
This page documents a story in Sprint 25. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
Goal
Decide and implement the FOMC-dated short end of the USD SOFR curve. The implementation task from the IR curve bootstrapping story was abandoned (2026-08-09) because it was premature: two model questions must be answered first, and both are design decisions, not implementation details.
- The unified tenor-resolution model. FOMC-dated pillars, IMM tenors (CDS) and ordinary anchor-offset tenors (rates) are three resolutions of the same abstraction. The model must cover all three cleanly — one mechanism, not three special cases.
- The calendar model. FOMC meeting dates are events of a given type on a currency calendar, not a new table per calendar. A central calendar per currency must hold events of different types (central-bank meetings, data releases, …) alongside holidays.
Once the model is decided and documented, this story re-scopes and implements the abandoned task: bootstrap the SOFR OIS short end off consecutive FOMC-meeting-dated pillars with flat-forward (step) interpolation, transitioning to a continuous method (log-linear or spline) at the long-end swap pillars, split at a defined tenor.
The analysis in this story is the deliverable of the first task; the
decision lands in * Decisions below when the analysis completes.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent sprint | Sprint 25 |
| Now | All five tasks DONE. D3 (fomc-bootstrap-segment) implemented, verified and merged via PR #1953 (2026-08-11): the FOMC short end as one config of the existing engine, seeds live in a recreated DB (schema 0.0.25), full build + rat + PR CI green. |
| Waiting on | Nothing. |
| Next | End-to-end feed verification of the FOMC segment (feed start, republish chain) needs the folder cascade to cover IR curve configs; tracked on the feed lifecycle harmonization story (task folder-cascade-all-asset-classes). |
| Last touched | 2026-08-11 |
Analysis
This section records the analysis carried out so far (2026-08-09,
when the story was created from the abandoned task). It is the input
to the analysis task; the task completes it and distils the decision
into * Decisions.
The bootstrap chain today: how a tenor becomes a pillar date
A bootstrap config (ir_curve_bootstrap_configs) names its pillar
list in ir_curve_bootstrap_pillars: one row per pillar, holding
start_tenor_code, end_tenor_code, curve_role_code
(DEPOSIT/FRA/SWAP) and sequence_index.
The curve_republish_resolver (ores.marketdata service) sorts the
pillars and, for each, resolves both tenor codes to dates and looks
up the observed rate. Two facts matter here:
- The end tenor code doubles as the point id into the raw grid
(
market_observations, published by the IR Rates synthetic feed). A pillar whose end code has no observed quote fails loudly. - A SWAP pillar's fixed-leg dates are the end dates of all prior pillars plus its own end date — so pillar order IS the fixed-leg schedule, and consecutive FOMC pillars would give exactly the FOMC-to-FOMC fixing schedule a short-end OIS needs.
The curve_bootstrap_engine (PR #1863) then solves tenor-by-tenor
for discount factors, honouring the config's
interpolation_method and day_count_convention. The engine
already accepts FLAT_FORWARD_THEN_LOG_LINEAR (equivalence-tested
against log-linear for local interpolation) and CUBIC_SPLINE
throws — so the flat-forward short end / continuous long end split
the FOMC task needs is already one configuration of the engine, not
engine work.
Tenor resolution today: one algorithm implemented
ores.refdata.api/tenor_resolution.cpp resolves a tenor code to a
date under a tenor_convention:
tenors(refdata) is a label catalog:code,kind(PERIOD/SPECIAL),unit,multiplier. Identity only.tenor_conventionspicks ameasured_fromanchor and aresolution_algorithm. Seeded:RATES_SPOT_FORWARD(ANCHOR_OFFSET),FX_SWAP_NEAR_LEG(ANCHOR_OFFSET),CREDIT_CDS_IMM(IMM_ROLL — seeded, never resolvable).tenor_convention_resolutionscarries per-tenoranchor_override,offset_unit,offset_multiplier.tenor_resolution_algorithmsdefines the vocabulary:ANCHOR_OFFSET(anchor + DAY/WEEK/MONTH/YEAR offset) andIMM_ROLL(step through the IMM quarterly roll schedule).
Runtime state: ANCHOR_OFFSET is implemented; IMM_ROLL throws
std::logic_error("IMM_ROLL resolution is not yet implemented").
The ROLL_QUARTER unit exists in the offset vocabulary and also
throws. The populate scripts document the IMM_ROLL gap explicitly:
the algorithm row and the CREDIT_CDS_IMM convention are seeded so the
model already accommodates a roll schedule, with no schema change
needed later — "see the runtime resolver's documented gap and the
corresponding capture".
Question 1: the unified FOMC/IMM/tenor model
Three resolution families exist or are needed:
ANCHOR_OFFSET(implemented): tenor = anchor + n x unit. Covers O/N, 1M, 1Y, … — pure calendar arithmetic.IMM_ROLL(seeded, unimplemented): tenor = the n-th IMM roll date. IMM dates are the first business day after 20 Mar/Jun/Sep/ Dec — exchange-fixed, four per year, and closed-form: they are derivable by rule, not looked up. CDS tenors resolve to the first business day following the appropriate roll date (disambiguated as "1Y 1RQ" — one year, one roll quarter).- FOMC (proposed): tenor = the n-th FOMC meeting date. FOMC dates are ~8 per year, published by the Fed years in advance, and are not closed-form — the exact dates shift year to year for scheduling reasons. They are a pure lookup into a fixed set.
The clean unification: a tenor resolves as "anchor + a count of steps along an axis". The axis is either calendar arithmetic (unit = DAY/WEEK/MONTH/YEAR) or a named schedule of dates (ROLL_QUARTER = the IMM roll schedule; a meeting axis = the FOMC schedule). The tenor row already has the (kind, unit, multiplier) shape; only new units and a schedule source are missing.
The sibling relationship is documented: FOMC Dates is "the same 'fixed schedule, not a horizon-relative offset' shape as IMM dates, but a different underlying calendar and a different purpose". The model must keep the schedule source general — IMM is derivable, FOMC must be data.
Question 2: the calendar model
The calendar cluster docs (Calendars/Holidays/Events hub, Diary/Effects model, Calendar Events, Holiday Administration) already prescribe the shape the user demands:
- The calendar is a diary: a validated, named enumeration of bare
time points, semantics-free. "A currency, or any other entity, can
need more than one calendar at once" — the currency-calendar
junction (
refdata_currency_calendars) exists. - Events are first-class diary entries distinct from holidays,
anchored to a time point, touching one or more currencies, and
carrying a
diary_entry_type:holiday,central_bank_meeting,data_release,other. The Calendar Events doc is explicit that central-bank meetings (FOMC, ECB Governing Council, BoE MPC, …) are exactly this: recurring, dated, well-known fixtures entered as a short run of instances against a lightweight "FOMC meeting" template — "rather than computed from a formula". - Template/Instance recurrence applies to holidays and events alike.
- Effects (typed downstream impact) are deliberately separate, attached by reference, so the calendar and calendar-event schema stay stable "regardless of how many kinds of effect are later modelled".
What exists in the schema today: calendars, calendar_types,
calendar_rules, calendar_exceptions, calendar_date,
currency_calendars, currency_pair_convention_calendar — the
holiday/business-day machinery. There is no calendar-event table
and no diary_entry_type vocabulary. The documented diary model is
ahead of the implementation.
FOMC dates therefore belong on a USD calendar as
central_bank_meeting events — the same single table that will hold
data releases and any other event type, not a new table per
calendar, and not a hardcoded resolution rule.
The connection: what the two questions share
FOMC_ROLL and IMM_ROLL are the same mechanism — "the n-th date in a schedule, on or after the anchor". They differ only in where the schedule comes from:
- IMM: closed-form rule (20th + business-day adjustment) — could be computed, or seeded as events; a design choice.
- FOMC: externally-announced data — must be events on a calendar.
So the resolution algorithm and the calendar-event store are the two
halves of one design: a roll algorithm walks a named schedule, and a
schedule is a typed event set on a calendar. The FOMC flat-forward
pillar list then falls out: pillars = (SPOT -> 1F), (1F -> 2F), …,
(nF -> split tenor), with split_tenor_code marking the boundary,
and the raw grid carrying quotes at the FOMC point ids.
Open design points (the analysis task decides these)
- The calendar-event entity: columns, whether events share the diary
with holidays (one
calendar_eventtable withdiary_entry_type) or sit besidecalendar_exceptions, and how Template/Instance recurrence lands for events. - The schedule source for roll algorithms: direct SQL lookup of events by (calendar, type) vs. a materialised schedule view.
- Whether IMM_ROLL migrates to the same event mechanism (IMM roll dates seeded as events) or stays on its closed-form path — the unified model should decide one way, not both ad hoc.
- Tenor notation for FOMC pillars (1F/2F vs. FOMC1 vs. MEETING1)
and the
ROLL_QUARTER-style unit name for the meeting axis. - The
measured_fromanchor andsplit_tenor_codesemantics for the FOMC segment under the existing config schema. - Seeding: the Fed's published FOMC calendar (federalreserve.gov) for a real data window; the Calendar Events doc surveys the G10 sources.
- The synthetic feed side: template entries must generate raw-grid quotes at the FOMC point ids (the old task's acceptance says "generated/bootstrapped off consecutive FOMC dates" — generation is in scope).
Acceptance
- A documented model decision in
* Decisionscovering: FOMC dates as typed events on a currency calendar (one calendar-event table for all event types — no table per calendar); and a unified tenor resolution model that coversANCHOR_OFFSET,IMM_ROLLand FOMC-roll without special-casing. - Domain docs (FOMC Dates, Interpolation, Tenor, IMM Dates, Calendar Events, Calendars/Holidays/Events) reflect the decided model — updating them if the implementation diverges from what is documented up front.
- The USD SOFR curve's OIS short end is bootstrapped off consecutive FOMC-meeting-dated pillars, not the ordinary tenor grid.
- The short-end interpolation between FOMC-dated pillars is a flat-forward step, distinct from the generic log-linear default; beyond the FOMC segment the curve transitions to a continuous non-local method at a defined split tenor.
- The raw grid (synthetic feed) carries observations at the FOMC pillar point ids, so the republish resolver's point-id lookup succeeds.
- The implementation is provable as one configuration of the
existing
curve_bootstrap_engineand config schema — not special-cased in engine code.
Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
| FOMC/IMM/tenor resolution + calendar events: model analysis and decision | DONE | 2026-08-09 | 2026-08-09 | Complete the analysis begun in the FOMC story: decide the calendar-event entity shape, the schedule source for roll algorithms, FOMC tenor notation, IMM_ROLL's fate, and the anchor/split-tenor semantics for the FOMC segment; land the decision in the story's * Decisions and update the domain docs. |
| Calendar event store: calendar_event + diary_entry_types + US.FOMC calendar | DONE | 2026-08-09 | 2026-08-10 | Implement the decided event-store model (story Decision D1): one calendar_event table with a diary_entry_type vocabulary – no table per calendar or per type. US.FOMC becomes a row in calendars; currency_calendars links USD to it. |
| Unified tenor resolution: tenor_schedules, SCHEDULE_STEP resolver, RATES_SPOT_FOMC, 1F..nF | DONE | 2026-08-09 | 2026-08-10 | Implement the decided unified resolution model (story Decision D2): a tenor resolves as anchor + calendar offset + n steps along a named schedule axis. tenor_schedules vocabulary (ROLL_QUARTER closed-form, FOMC_MEETING event-lookup), SCHEDULE_STEP branch in tenor_resolution.cpp, RATES_SPOT_FOMC convention with 1F..nF tenors, CREDIT_CDS_IMM migrated onto the schedule model. |
| Seed the 2025-2027 FOMC meeting dates from federalreserve.gov | DONE | 2026-08-09 | 2026-08-10 | Implement the decided seeding (story Decision D4): a real window of FOMC meeting dates as central_bank_meeting calendar_event rows on US.FOMC, transcribed from the Fed's published calendar, each row carrying source='federalreserve.gov'. |
| FOMC bootstrap segment: pillars SPOT->1F..nF->split under one config | DONE | 2026-08-10 | 2026-08-11 | Implement the decided FOMC segment (story Decision D3): bootstrap the SOFR OIS short end off consecutive FOMC-dated pillars as one configuration of the existing engine – pillars SPOT->1F, 1F->2F, …, nF->split under ir_curve_bootstrap_configs, interpolation FLAT_FORWARD_THEN_LOG_LINEAR, split_tenor_code at the boundary, raw-grid quotes at the FOMC point ids. |
Decisions
D1 — One event store, typed entries: calendar_event + diary_entry_type
Decided 2026-08-09 by the analysis task. FOMC dates are events of a given type on a currency calendar: one table for all event kinds, never a table per calendar or per type.
- New
calendar_evententity (ores.refdata, codegen'd, bitemporal, sibling ofcalendar_rules=/=calendar_exceptions):id(uuid),calendar_code→calendars,event_date(date),diary_entry_type→ newdiary_entry_typesvocabulary,name,description(nullable),source(nullable provenance, e.g.'federalreserve.gov'), plus the standard audit/temporal columns. - New
diary_entry_typesenumeration (code/name/description/ display_order), seededholiday,central_bank_meeting,data_release,other— open-ended; a future kind costs one row. - The FOMC calendar is a row in the existing
calendarstable: codeUS.FOMC,calendar_type'central_bank_meeting', =country_code'US', =source'federalreserve.gov', =is_editabletrue. Thecalendar_typesvocabulary already seedscentral_bank_meetinganddata_release, so the calendar vocabulary exists — no new table. - Holidays keep their existing machinery (
calendar_rules,calendar_exceptions,calendar_date);calendar_eventholds the non-holiday kinds. Theholidayentry type stays in the vocabulary so the whole classification lives in one place. - Template/Instance recurrence: an event row is an instance; its
template is the (calendar, diary_entry_type, name) triple. FOMC
meetings are entered as a short run of instances — the Calendar
Events doc's textbook case. Formulaic recurrence generation (e.g.
Non-Farm Payrolls, first Friday) is deferred;
calendar_rules's grammar can later feed a template link if a consumer needs it. currency_calendarslinksUSD↔US.FOMCso the meeting calendar is discoverable per currency.
D2 — Unified tenor resolution: SCHEDULE_STEP + a schedule vocabulary
A tenor resolves as anchor + n steps along an axis. Two axes:
calendar arithmetic (ANCHOR_OFFSET, unchanged) and a named schedule
(new). IMM and FOMC are the same mechanism; they differ only in where
the schedule comes from.
- New
tenor_schedulesvocabulary (code/name/description/ display_order/schedule_source/calendar_code/diary_entry_type):ROLL_QUARTER(CLOSED_FORM — the IMM rule, 20th + business-day adjustment, stays code-side as the derivable schedule) andFOMC_MEETING(EVENT_LOOKUP onUS.FOMC/central_bank_meeting— the externally-announced schedule, data not formula). tenor_convention_resolutionsgains nullableschedule_codeandschedule_step_count(required when the convention's algorithm isSCHEDULE_STEP). The offset columns stay the calendar-axis offset; the schedule columns are the schedule axis. Resolved date = walk(anchor + calendar offset, schedule, steps).tenor_resolution_algorithmsgainsSCHEDULE_STEP.IMM_ROLLis superseded:CREDIT_CDS_IMMmigrates to (SCHEDULE_STEP,ROLL_QUARTER) with resolution rows carrying offset (YEAR × 1) + schedule (ROLL_QUARTER, 1 step) — one mechanism for IMM and FOMC, not two special cases. TheIMM_ROLLalgorithm row is removed from populate once no convention references it; theIMM_ROLLanchor (measured_from) stays.- FOMC notation: tenors
1F,2F, …,nF(kindSPECIAL, unitNONE, multipliern— mirroringO/N's SPECIAL/NONE shape); conventionRATES_SPOT_FOMC(measured_fromSPOT, algorithmSCHEDULE_STEP, scheduleFOMC_MEETING); resolution rows1F..=nF= withschedule_step_count= n → the n-th meeting on-or-after spot.
D3 — FOMC segment as one config
Pillars SPOT→1F, 1F→2F, …, 8F→1Y under the existing
ir_curve_bootstrap_configs schema: interpolation_method
FLAT_FORWARD_THEN_LOG_LINEAR covers the segment, split_tenor_code
= 1Y marks the boundary, and the end-tenor code doubles as the point
id into the market_observations raw grid — the republish resolver's
lookup and the engine both work unchanged. No engine change; no
schema change beyond D1/D2.
The concrete values are settled (2026-08-10): split_tenor_code 1Y
— the codegen default, where the Fixed-vs-Compounded-SOFR swap grid
begins — and n = 8 (1F..=8F=, eight meetings ≈ one year). The
ir_curve_bootstrap_configs table is unseeded today; D3 seeds the
first config.
D4 — Seeding
Seed a 2025–2027 window of FOMC meeting dates transcribed from
federalreserve.gov/monetarypolicy/fomccalendars.htm as
calendar_event rows (central_bank_meeting) on US.FOMC,
source'federalreserve.gov'=. No formal feed exists (per Calendar
Events) — transcription with the citable source.
Transcribing convention (2026-08-10): one event per meeting, dated the second day of the published two-day range — the statement/decision day, usually a Wednesday. The 2027 schedule is fully published (Fed release 2025-09-05, including the first 2028 meeting), so all three years seed 8 meetings each; dates remain tentative until the preceding meeting confirms them.
Out of scope
- Dual-curve discounting math (PROJECTION curves discounted off a separate FUNDING curve) — tracked on the generation side already.
- The Effects half of the Diary/Effects model (vol variance weights, deliverability restrictions, P&L attribution).
- Reconciliation of the bootstrap engine against QuantLib's own implementation.