From CSV to Real-Time: Curve Provider Architecture for Multi-Source Bond Markets
How BondFoundry ingests yield curves from ECB, US Treasury, Bloomberg BVAL, Refinitiv RDP, and CSV — with stale-curve detection that survives the morning.
A bond pricing engine is only as good as the curve under it. A curve provider abstraction is only as good as how it handles the day the upstream feed is silent.
This is the part of fixed-income systems that does not make the LinkedIn deck: how the curve gets in, how it survives a missing print, and how the downstream agent knows when the answer is stale.
The provider interface
BondFoundry treats every curve source — ECB, US Treasury, Bloomberg BVAL, Refinitiv RDP, CSV upload — as an implementation of one Python protocol:
class CurveProvider(Protocol):
name: str
async def fetch(self, valuation_date: date) -> RawCurve: ...
def parse(self, raw: RawCurve) -> ParsedCurve: ...
def freshness_signal(self, parsed: ParsedCurve) -> CurveFreshness: ...
Three methods. fetch is provider-specific I/O. parse normalizes to a (tenor, rate) series in a canonical day-count and compounding convention. freshness_signal is the one most implementations under-think — and the one that matters most in production.
Why freshness is its own method
A curve fetched at 9 AM with a valuation_date of yesterday looks fine to the schema. It looks wrong to a PM about to mark a book.
The freshness signal returns one of four states:
LIVE— published today, within tolerance for the venue (e.g. ECB publishes by 16:15 CET)STALE_PRINT— the provider returned a previous business day’s curve unchangedSTALE_HOURS— the curve is today’s but published outside the expected windowUNKNOWN— provider did not communicate freshness; flag for review
The pricing engine refuses to run on STALE_PRINT without an explicit override (T2, single-HITL). The agent, asked to price a bond on a stale curve, returns the action to the policy gate which routes it to HITL with the verbatim text “curve freshness=STALE_PRINT; explicit override required.”
That clause carries a framework_ref to AIR-DET-21 because it is a detection control — the system notices the stale state before the operator does.
Provider-specific quirks
ECB. The most disciplined publisher. The euro-area yield curve hits the API by 16:15 CET on every TARGET2 business day. The provider validates the <TimeStamp> element and computes freshness from it.
US Treasury. Daily yield curve rates publish around 4:00 PM ET. The XML feed is structured, but the historical archive uses a different schema. The provider normalizes both into the canonical shape.
Bloomberg BVAL. The richest source — and the trickiest. BVAL prints carry a quality score (1–10) that has to be carried through to the audit row. A BVAL-priced bond is not the same artifact as a UST-curve-derived price, and downstream consumers need to know which.
Refinitiv RDP. Token-based auth with TTL caching to avoid hammering the auth endpoint on every call. Curve definitions vary by venue and need explicit mapping to the canonical convention.
CSV. The bring-your-own option. Validates a strict header schema and runs the same freshness rules as live providers (CSV upload timestamp counts as the print time).
Adding a provider
The repo has an adding-a-curve-provider.md walkthrough. The shape is:
- Implement the
CurveProviderprotocol inpackages/bondfoundry_engine/curves/providers/ - Add the venue-specific calendar (settlement, holidays) to the calendar registry
- Add at least one freshness fixture to the test corpus
- Add an eval case for the
STALE_PRINTpath — this is the one most providers skip - Register the provider in the curve-provider manifest
The eval case for the stale path is the one that proves the freshness signal works end-to-end. The provider passes when, given a stale upstream fixture, the downstream pricing call routes through the policy gate with the expected verbatim text.
What this buys the desk
Three things you can show the operator:
- A unified curve view across venues. Same canonical convention, same freshness display, regardless of upstream source.
- A clear escalation when the data goes thin. The agent says “I can price this; the curve is two hours stale — confirm to proceed” instead of returning a number that looks fine and is wrong.
- An audit row per pricing call that records the curve source, the freshness state, and (for BVAL) the quality score.
The desk’s morning routine becomes: review the curve freshness panel, approve any explicit overrides on stale curves, run the day. The agent is doing the boring middle of the workflow with the right control points exposed.
Where the freshness rules came from
We learned the rules from the desks that broke them. The original implementation accepted any curve the provider returned, with freshness_signal returning a single True/False. The first production deployment ran for three weeks before a BVAL feed lag at month-end produced a curve that was technically “today” but printed against stale underlying quotes.
The four-state model came out of that postmortem. The general lesson: freshness is a spectrum, not a boolean, and the system has to expose the spectrum to the human at the routing layer — not collapse it into a yes/no in code.
See the curve provider source on GitHub. For the full architecture, see The Four Pillars of Governed AI in Finance.