Task: The feed seam: common producer interface + per-kind factory
Table of Contents
This page documents a task in the Feed lifecycle harmonization: asset-class-agnostic feed control-plane story. It captures the goal, current status, acceptance, and any notes or results.
Goal
The seam the original FX PoC architecture doc specified (design question 8, feed manager) finally exists: one common feed interface every producer implements, and a factory mapping asset class to producer builder. FX and IR curve producers are both behind it; every control-plane piece (folder cascade, per-config handler, controller, auto-start, ingest) dispatches through it.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Feed lifecycle harmonization: asset-class-agnostic feed control-plane |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-08-11 |
Acceptance
- A common feed interface exists (generalizing IFxSpotFeed) exposing the shape both producers already have: source_name, qualifier, role, start, stop, and a conflict key for qualifier+role semantics.
- fx_spot_feed and ir_curve_feed both implement it; ir_curve_feed gains the interface it currently lacks.
- A factory maps asset class to producer builder; both kinds are constructible through it. The factory is the registration point for future asset classes.
- The factory lives where the doc specified: ores.marketdata.api hosts the interface; ores.synthetic.api registers the producers.
Plan
The seam is built in one pass over the producers, then wired through the factory:
- Interface:
IFeedreplacesIFxSpotFeedin ores.marketdata.api (include/ores.marketdata.api/domain/i_feed.hpp). It exposes the shape both producers already have —source_name(),qualifier(),role(),start(),stop(),publish_count()— plus aconflict_key()that joins (qualifier, role) with a unit separator (the IR qualifier+role conflict semantics, expressed by the feed itself).start()drops the on_tick handler: the only caller passes a no-op, and both feeds publish to NATS themselves.ore_key()stays a concrete accessor onfx_spot_feed(FX-specific, no IR analog). Producers move into ores.synthetic.api (
include/ores.synthetic.api/feeds/src/feeds/, namespaceores::synthetic::feed,ORES_SYNTHETIC_API_EXPORT):
fx_spot_feed,ir_curve_feed,ir_curve_template_resolver, and themake_*_feedbuilders.fx_spot_feedgains asource_nameconstructor parameter and derives its qualifier from its ore_key viasplit_market_series_keyat construction;ir_curve_feedgains: public IFeedandconflict_key().synthetic_producer_subjectmoves with the FX producer so the factory builds subjects the same way the controller does.- Factory:
feed_factoryin the api maps a kind string to a producer builder (feed_build_contextwith the nats client pair + caller bearer token, and per-kind build inputs in a variant).make_default_feed_factory()registers both kinds — the registration point for future asset classes.make_fx_spot_feedbuilds from the persistedfx_spot_generation_config+gmm_componentrows (mirroring the auto-start path; the FX vintage resolution stays infeed_controller::startuntil the client-supplied-params path is deleted by the control-plane task). - Service wiring: both auto-start walks dispatch through the factory. The FX
controller gains
add()(mirroringcurve_feed_controller::add); both controllers acceptIFeed. The ad-hocstart()path keeps its inline construction until the per-config control-plane task deletes it. - The api CMakeLists gains the producer deps (nats, marketdata.api,
marketdata.client/core, database, refdata.core);
component_files.cmakeis regenerated; a factory test in the api tests covers both kinds, kind enumeration, and wrong-input rejection.
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 |
|---|---|
| #1955 | [marketdata,synthetic] Feed producer seam: common IFeed interface + per-kind factory |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | FX auto-start walk has no per-row try/catch, unlike the IR walk | projects/ores.synthetic/service/src/app/application.cpp | Declined | Pre-existing on main, deliberately preserved by the seam design. The claimed failure modes are unreachable: the walk hardcodes fx_spot_feed_kind, registered by the same make_default_feed_factory() call, and constructs the exact build-input variant the builder expects; empty components are already skip-with-warn. The uniform-auto-start task (E1F81BE2) merges the walks and makes skip-with-warn common code. |
| 2 | FX tick publish not guarded; an uncaught throw would std::terminate() the service |
projects/ores.synthetic/api/src/feeds/fx_spot_feed.cpp | Accepted | Publish wrapped in try/catch, logging "SYNTHETIC PUBLISH FAILED" and skipping the tick, mirroring ir_curve_feed::start(). Fixed in fde4bc008. |
| 3 | Doc overstates make_fx_spot_feed's error behaviour |
projects/ores.synthetic/api/include/ores.synthetic.api/feeds/fx_spot_feed.hpp | Accepted | @throws corrected: throws only on empty components and non-positive ticks_per_hour; an unknown process_type falls back to the geometric engine, not an exception. Fixed in 252fa53cc. |
Result
The seam is built and wired end to end:
IFeedreplacesIFxSpotFeedin ores.marketdata.api (domain/i_feed.hpp):source_name(),qualifier(),role(),conflict_key(),start(),stop(),publish_count().start()takes no handler; both producers publish to NATS themselves.ore_key()remains a concrete accessor onfx_spot_feed.- Both producers implement it.
fx_spot_feed,ir_curve_feedandir_curve_template_resolvermoved to ores.synthetic.api (feeds/, namespaceores::synthetic::feed,ORES_SYNTHETIC_API_EXPORT);synthetic_producer_subjectmoved with the FX producer.make_fx_spot_feedbuilds an FX feed from its persisted config plusgmm_componentrows (the factory path leaves the FX vintage lookup infeed_controller::startuntil the control-plane task deletes it);make_ir_curve_feedbuilds the IR feed, vintage resolution included. feed_factorymaps a kind string to a producer builder:feed_build_context(nats client pair + caller bearer token) and per-kind build inputs in a variant;make_default_feed_factory()registersfx_spotandir_curve— the registration point for future asset classes.- Service wiring: both auto-start walks dispatch through the factory; both
controllers accept
IFeed;feed_controller::add()downcasts internally for status logging. - The api CMakeLists gained the producer deps;
component_files.cmakeregenerated;feed_factory_tests.cppcovers both kinds, kind enumeration, unknown-kind and wrong-input rejection, and replacement registration.
Acceptance: the interface exists and generalizes IFxSpotFeed; both producers
implement it (ir_curve_feed gained the interface it lacked); the factory
constructs both kinds; the interface lives in ores.marketdata.api and the
producers register in ores.synthetic.api. All met.
Verification: full build green; compass build rat green (60 suites); the new
factory test 6/6 passing; the synthetic service restarted on the new build.