Story: Synthetic data collections: Basic and Realistic
Table of Contents
This page documents a story in Sprint 23. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
Carried from Sprint 22 (6/8 tasks done). Sprint 22's Stories table incorrectly marked this ABANDONED; the story doc records no such decision — its blocker (shell import command) resolved via a since-DONE spun-off story, so it is being carried forward as real remaining work.
Goal
Synthetic data librarian support: FX foundation gave every party a single synthetic FX
collection (the ore_analytics dataset bundle).
This story adds a second sample collection so parties can pick between
two archetypes, and closes the gaps that surfaced while designing it:
- Two named collections:
Basic(simple/exaggerated process dynamics, easy to eyeball on a chart while exercising every UI feature) andRealistic(calibrated to look like actual FX behaviour). Both seed their initial spot from the same real reference data — the difference between them is process dynamics only, not the starting point. A bad/arbitrary starting spot isn't "basic," it's just wrong. - Reference/vintage data as market data, not a synthetic-service
concept: a "vintage" is real, dated
market_observationrows (asset_class::fx, tagged with a distinguishingsource, e.g.ore.reference), imported once from ORE's own bundled example data intoores.marketdata— reusing the exact schema fields already designed for this (source,point_id,observation_datetime), not a new table or a synthetic-specific store.ores.syntheticis a consumer of this at feed-start (asksores.marketdata"what wasFX/RATE/EUR/USDas of2016-02-05from sourceore.reference?"), never a store of it — so it's reusable by future IR/vol-surface work without any synthetic-specific plumbing, and scales to more vintages as just more dated rows, never more collection names. - Market Simulator UI groups by collection, not by feed type: today
the tree's top-level node is a feed-source label ("Synthetic FX
Spot: EUR/USD") with all configured pairs flattened under it
regardless of which collection they belong to. It should group by
collection (
Basic/Realistic) instead, since every entry can be enabled simultaneously across collections and users need to tell them apart at a glance. - Feed/subject namespacing by collection: NATS subjects and
source_namevalues collide across collections today (both would publish assynthetic.eurusd). Prefix with the collection:synthetic.basic.eurusd/synthetic.realistic.eurusd. - Vintage-availability guard: if the reference-data import hasn't
been run, starting a
Realisticfeed should fail with an actionable error pointing at the import, not silently default to a wrong value or crash deep in feed-start logic.
Prior investigation (this session)
- ORE's bundled example data is anchored overwhelmingly to one date:
2016-02-05appears in 288 of 392 exampleInput/*.xmlasofDateparameters (≈73%); the remainder is a long tail of one-off dates, no second dominant cluster. - Real FX spot values as of that date, from
external/ore/examples/Legacy/Example_56/Input/market.txt(the richest FX quote set at that date): EURUSD1.132337, GBPUSD ≈=1.3946= (derived via EUR/GBP), EURCHF1.125046, USDCHF ≈=0.9936=, EURJPY128.15. market.txtformat: plain whitespace-delimited<date> <quote-name> <value>, e.g.20160205 FX/RATE/EUR/USD 1.132337— trivial to parse.ores.marketdata.core::service::import_servicealready exists (projects/ores.marketdata/core/include/ores.marketdata.core/service/import_service.hpp) — this story reviews/extends it rather than building import functionality from scratch.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent sprint | Sprint 23 |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-11 |
Acceptance
- Existing
ores.marketdataimport functionality (import_service=/=import_protocol) is reviewed against the need to import ORE'smarket.txtreference data as datedmarket_observationrows tagged with a distinguishing source; extended if needed, not rebuilt. - A
Basicand aRealisticdataset bundle both exist (alongside the existingore_analyticsbundle, or replacing/extending it — decide during implementation), each seeding its FX configs' initial price from the same imported2016-02-05reference vintage. BasicandRealisticdiffer only in GMM/process parameters, not in starting spot.ores.syntheticvalidates vintage-data availability before a feed that depends on it starts — both as an on-demand "validate every entry" toolbar action in Market Simulator, and as an implicit check at feed-start — surfacing a clear, actionable error (pointing at the import step) rather than a silent bad default or a deep crash.- Market Simulator's feed tree groups by collection (
Basic/Realistic) as the top-level node, not by feed-source label. - Synthetic feed
source_name=/NATS subject is namespaced by collection: =synthetic.basic.<pair>/synthetic.realistic.<pair>, so two collections can run the same pair concurrently without colliding.
Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
| Review and extend ores.marketdata import for ORE reference vintages | DONE | 2026-07-03 | 2026-07-03 | Review existing import_service/import_protocol against the need to import ORE market.txt data as dated, source-tagged market_observation rows; extend as needed. |
| Import the 2016-02-05 ORE reference vintage | DONE | 2026-07-05 | 2026-07-06 | Import FX spot values from external/ore/examples/Legacy/Example_56/Input/market.txt into ores.marketdata as market_observation rows tagged source=ore.reference. |
| Vintage-availability guard in ores.synthetic | DONE | 2026-07-06 | 2026-07-09 | Validate required vintage data exists in ores.marketdata before a dependent feed starts, plus a Market Simulator toolbar action to validate every configured entry on demand. |
| Seed Basic and Realistic dataset bundles | DONE | 2026-07-10 | 2026-07-10 | Create the Basic and Realistic dataset bundles, both seeding initial spot from the imported 2016-02-05 vintage, differing only in GMM/process parameters. |
| Namespace feed source_name/NATS subject by collection | DONE | 2026-07-15 | 2026-07-17 | Prefix synthetic feed source_name and NATS subjects with the owning collection (synthetic.basic.<pair> / synthetic.realistic.<pair>) so concurrent collections don't collide. |
| Group Market Simulator feed tree by collection | DONE | 2026-07-17 | Change the Market Data Feeds tree's top-level node from a feed-source label to the owning collection (Basic / Realistic). | |
| Detect and correct reversed FX spot quotes during market data import | DONE | 2026-07-09 | 2026-07-10 | Wire fx_quote_convention_checker into ores.marketdata's import_service, fed from ores.refdata's currency_pair reference data, so a reversed FX/RATE key (e.g. the Legacy/Example_56 FX/RATE/USD/GBP quirk) is auto-corrected and clearly reported at import time instead of silently mis-keying market data. |
| Publish a curated FX driver-rate dataset for 2016-02-05 | DONE | 2026-07-10 | 2026-07-10 | Publish real Federal Reserve H.10 FX driver rates across 25 vintages spanning 2016 as a DQ/librarian dataset, replacing reliance on ORE's own approximate bundled vintage. |
| Server-computed vintage-validity indicator per feed and folder | DONE | 2026-07-16 | 2026-07-17 | Read-time computed is_vintage_valid boolean per fx_spot_generation_config (null for fixed-price feeds), surfaced in the Qt tree per-feed and rolled up per-folder client-side, replacing the on-demand-only 'Validate Vintage' action with an always-visible status. |
Decisions
- Discovered while starting the vintage-import task: no non-interactive
way exists to trigger
import_market_data_request(only the QtImportTradeDialogcalls it). Spun off as its own story, ores.shell command for market data import, rather than building shell tooling inline here — it's independently useful (any future scripted/provisioning-time import needs it too), and keeps this story's tasks focused on collections rather than shell infrastructure. Import the 2016-02-05 ORE reference vintage is blocked on it. - Also found while sourcing reference values: a systematic bug across
several vendored ORE example
market.txtfiles —FX/RATE/USD/GBPandFX/RATE/USD/CHFrows storing values that are mathematicallyEURUSD/EURGBP=/=EURUSD/EURCHF(i.e. the GBP/CHF-in-USD rate), not USD-in-GBP/CHF as the key order implies, inconsistent with every otherEUR/xxxrow in the same files. Reported upstream to the ORE project with the full file list (10+ instances found acrossLegacy,ORE-API,ORE-Python,XvaRisk,InitialMargin). Only fixed locally where provably safe:XvaRisk/Input/market_20160205_eonia_200bp_up.txt,XvaRisk/Input/market_20160205_eur6m_200bp_up.txt, andORE-Python/Notebooks/Example_7/Input/market.txt— each has its own localcurveconfig.xmlwith zero reference to the old key anywhere. Left unfixed (bug still present in the vendored tree, deliberately not patched):Legacy/Example_56,Legacy/Example_44,InitialMargin/Input/Simm/simm_market.txt— these three have no localcurveconfig.xmland resolve the sharedexternal/ore/examples/Input/curveconfig.xml(viacurveConfigFile = ../../../Input/curveconfig.xmlin their SIMM run configs), which itself has a literal<SpotRate>FX/RATE/USD/GBP</SpotRate>=/=<SpotRate>FX/RATE/USD/CHF</SpotRate>wiring — initially fixed then reverted after a PR review caught that this shared-config path couldn't be ruled out as live without actually running the SIMM examples.ORE-API,ORE-Python/Notebooks/Example_9,Example_4,Example_3were investigated and left unfixed for the same reason (their own localcurveconfig.xmldirectly wires the old key). The ~20 further instances the upstream issue lists beyond these were not individually verified safe/unsafe — flagged in the issue for ORE to triage, not something this story attempted to fix. - Fixing the same bug's blind spot:
import_service's fixings-import loop didn't setmarket_fixing.sourceeven though the field exists with identical intent tomarket_observation.source— the observations loop got tagged, fixings didn't. Fixed symmetrically. - Vintage/reference data lives in
ores.marketdataas ordinary (dated, source-tagged)market_observationrows, not as a new synthetic-specific table or store.ores.syntheticqueries it at feed-start; it never owns or caches a private copy. Rationale: reuses the exact schema fields (source,point_id,observation_datetime) already designed for this in the IR Rates story's investigation; scales to more vintages as more rows, not more collection names or schema changes; reusable by future IR/vol-surface work for free. - Collections differ only in process parameters, never in starting
spot — both
BasicandRealisticseed from the same reference vintage. A collection that starts from a nonsensical spot isn't "basic," it's broken. - Vintage-availability validation is both a toolbar "validate all" action and an implicit start-time check — not one or the other. The toolbar action lets a user audit an entire collection proactively; the start-time check is the safety net for anyone who skips it.
Out of scope
- Adding vintages beyond
2016-02-05(this story imports and uses one; the reference-data mechanism is designed to support more later without rework, but adding a second vintage is follow-on work). - Asset classes beyond FX (matches the scope boundary already set by the FX foundation story).
- Curve bootstrapping / vol surfaces (separate stories, per IR Rates synthetic data generation's scope reasoning).
Result
Both Basic and Realistic collections run concurrently for the same party
with no NATS subject or market_observation.source collisions. What
started as a namespacing task grew into a real, queryable hierarchy: a
generic self-referencing ores.synthetic.folder entity (root > collection
> asset class > instrument type > feed) drives the Market Simulator tree
directly, and backs a folder-scoped start/stop request so "start everything
under Basic" (or the whole Synthetic root) is one server-resolved call
instead of per-feed client enumeration – usable identically from Qt,
ores.shell, or a wt workflow step. A vintage-validity indicator (computed
live, not persisted, to stay accurate across data imports with no feed
edit) rounds it out, shown as small badge emblems on the tree rather than
separate columns after manual QA flagged the first cut as unreadable.
Two real bugs were found and fixed along the way, both worth remembering:
market_data_generation_config had party_id wrongly declared a Natural
Key (generating a DB constraint that capped a party at one collection
ever), and the folder-scoped start endpoint initially failed to forward
the caller's bearer token into the vintage-availability check, so it ran
under the service's own system-tenant identity instead of the caller's and
silently saw no data for any real tenant.